@macroforge/mcp-server 0.1.37 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/docs/api/api-overview.md +13 -13
  2. package/docs/api/expand-sync.md +8 -8
  3. package/docs/api/native-plugin.md +15 -15
  4. package/docs/api/position-mapper.md +6 -6
  5. package/docs/api/transform-sync.md +11 -11
  6. package/docs/builtin-macros/clone.md +43 -23
  7. package/docs/builtin-macros/debug.md +50 -18
  8. package/docs/builtin-macros/default.md +79 -28
  9. package/docs/builtin-macros/deserialize/cycleforward-reference-support.md +11 -0
  10. package/docs/builtin-macros/deserialize/example.md +1625 -0
  11. package/docs/builtin-macros/deserialize/overview.md +15 -10
  12. package/docs/builtin-macros/deserialize/union-type-deserialization.md +27 -0
  13. package/docs/builtin-macros/deserialize/validation.md +34 -0
  14. package/docs/builtin-macros/deserialize.md +1608 -23
  15. package/docs/builtin-macros/hash.md +87 -20
  16. package/docs/builtin-macros/macros-overview.md +40 -40
  17. package/docs/builtin-macros/ord.md +56 -31
  18. package/docs/builtin-macros/partial-eq/example.md +526 -0
  19. package/docs/builtin-macros/partial-eq/overview.md +39 -0
  20. package/docs/builtin-macros/partial-eq.md +184 -26
  21. package/docs/builtin-macros/partial-ord.md +68 -30
  22. package/docs/builtin-macros/serialize/example.md +139 -0
  23. package/docs/builtin-macros/serialize/overview.md +32 -0
  24. package/docs/builtin-macros/serialize/type-specific-serialization.md +22 -0
  25. package/docs/builtin-macros/serialize.md +130 -28
  26. package/docs/concepts/architecture.md +2 -2
  27. package/docs/concepts/derive-system.md +25 -39
  28. package/docs/concepts/how-macros-work.md +8 -4
  29. package/docs/custom-macros/custom-overview.md +23 -23
  30. package/docs/custom-macros/rust-setup.md +31 -31
  31. package/docs/custom-macros/ts-macro-derive.md +107 -107
  32. package/docs/custom-macros/ts-quote.md +226 -226
  33. package/docs/getting-started/first-macro.md +38 -28
  34. package/docs/getting-started/installation.md +15 -15
  35. package/docs/integration/cli.md +9 -9
  36. package/docs/integration/configuration.md +16 -16
  37. package/docs/integration/mcp-server.md +6 -6
  38. package/docs/integration/svelte-preprocessor.md +40 -41
  39. package/docs/integration/typescript-plugin.md +13 -12
  40. package/docs/integration/vite-plugin.md +12 -12
  41. package/docs/language-servers/zed.md +1 -1
  42. package/docs/sections.json +88 -2
  43. package/package.json +2 -2
@@ -8,18 +8,10 @@ safe parsing of complex JSON structures including circular references.
8
8
 
9
9
  | Type | Generated Code | Description |
10
10
  |------|----------------|-------------|
11
- | Class | `static fromStringifiedJSON()`, `static fromObject()`, `static __deserialize()` | Static factory methods |
12
- | Enum | `fromStringifiedJSONEnumName(json)`, `__deserializeEnumName(data)`, `isEnumName(value)` | Standalone functions |
13
- | Interface | `fromStringifiedJSONInterfaceName(json)`, `fromObjectInterfaceName(obj)`, etc. | Standalone functions |
14
- | Type Alias | `fromStringifiedJSONTypeName(json)`, `fromObjectTypeName(obj)`, etc. | Standalone functions |
15
-
16
- ## Configuration
17
-
18
- The `functionNamingStyle` option in `macroforge.json` controls naming:
19
- - `"prefix"` (default): Prefixes with type name (e.g., `myTypeFromStringifiedJSON`)
20
- - `"suffix"`: Suffixes with type name (e.g., `fromStringifiedJSONMyType`)
21
- - `"generic"`: Uses TypeScript generics (e.g., `fromStringifiedJSON<T extends MyType>`)
22
- - `"namespace"`: Legacy namespace wrapping
11
+ | Class | `classNameDeserialize(input)` + `static deserialize(input)` | Standalone function + static factory method |
12
+ | Enum | `enumNameDeserialize(input)`, `enumNameDeserializeWithContext(data)`, `enumNameIs(value)` | Standalone functions |
13
+ | Interface | `interfaceNameDeserialize(input)`, etc. | Standalone functions |
14
+ | Type Alias | `typeNameDeserialize(input)`, etc. | Standalone functions |
23
15
 
24
16
  ## Return Type
25
17
 
@@ -64,7 +56,7 @@ The macro supports 30+ validators via `@serde(validate(...))`:
64
56
 
65
57
  The `@serde` decorator supports:
66
58
 
67
- - `skip` / `skip_deserializing` - Exclude field from deserialization
59
+ - `skip` / `skipDeserializing` - Exclude field from deserialization
68
60
  - `rename = "jsonKey"` - Read from different JSON property
69
61
  - `default` / `default = expr` - Use default value if missing
70
62
  - `flatten` - Read fields from parent object level
@@ -72,8 +64,8 @@ The `@serde` decorator supports:
72
64
 
73
65
  ## Container-Level Options
74
66
 
75
- - `deny_unknown_fields` - Error on unrecognized JSON properties
76
- - `rename_all = "camelCase"` - Apply naming convention to all fields
67
+ - `denyUnknownFields` - Error on unrecognized JSON properties
68
+ - `renameAll = "camelCase"` - Apply naming convention to all fields
77
69
 
78
70
  ## Union Type Deserialization
79
71
 
@@ -89,7 +81,7 @@ For unions containing primitive types (`string | number`), the deserializer uses
89
81
 
90
82
  ### Class/Interface Unions
91
83
  For unions of serializable types (`User | Admin`), the deserializer requires a
92
- `__type` field in the JSON to dispatch to the correct type's `__deserialize` method.
84
+ `__type` field in the JSON to dispatch to the correct type's `deserializeWithContext` method.
93
85
 
94
86
  ### Generic Type Parameters
95
87
  For generic unions like `type Result<T> = T | Error`, the generic type parameter `T`
@@ -105,28 +97,1621 @@ Mixed unions (e.g., `string | Date | User`) check in order:
105
97
 
106
98
  ## Example
107
99
 
100
+ ```typescript before
101
+ /** @derive(Deserialize) @serde({ denyUnknownFields: true }) */
102
+ class User {
103
+ id: number;
104
+
105
+ /** @serde({ validate: { email: true, maxLength: 255 } }) */
106
+ email: string;
107
+
108
+ /** @serde({ default: "guest" }) */
109
+ name: string;
110
+
111
+ /** @serde({ validate: { positive: true } }) */
112
+ age?: number;
113
+ }
114
+ ```
115
+
116
+ ```typescript after
117
+ class User {
118
+ id: number;
119
+
120
+ email: string;
121
+
122
+ name: string;
123
+
124
+ age?: number;
125
+ }
126
+ ```
127
+
128
+ Generated output:
129
+
130
+ ```typescript
131
+ class User {
132
+ id: number;
133
+
134
+ email: string;
135
+
136
+ name: string;
137
+
138
+ age?: number;
139
+ }
140
+ ```
141
+
142
+ Generated output:
143
+
144
+ ```typescript
145
+ import { DeserializeContext } from 'macroforge/serde';
146
+ import { DeserializeError } from 'macroforge/serde';
147
+ import type { DeserializeOptions } from 'macroforge/serde';
148
+ import { PendingRef } from 'macroforge/serde';
149
+
150
+ /** @serde({ denyUnknownFields: true }) */
151
+ class User {
152
+ id: number;
153
+
154
+ email: string;
155
+
156
+ name: string;
157
+
158
+ age?: number;
159
+
160
+ constructor(props: {
161
+ id: number;
162
+ email: string;
163
+ name?: string;
164
+ age?: number;
165
+ }) {
166
+ this.id = props.id;
167
+ this.email = props.email;
168
+ this.name = props.name as string;
169
+ this.age = props.age as number;
170
+ }
171
+
172
+ /**
173
+ * Deserializes input to an instance of this class.
174
+ * Automatically detects whether input is a JSON string or object.
175
+ * @param input - JSON string or object to deserialize
176
+ * @param opts - Optional deserialization options
177
+ * @returns Result containing the deserialized instance or validation errors
178
+ */
179
+ static deserialize(
180
+ input: unknown,
181
+ opts?: DeserializeOptions
182
+ ): Result<
183
+ User,
184
+ Array<{
185
+ field: string;
186
+ message: string;
187
+ }>
188
+ > {
189
+ try {
190
+ // Auto-detect: if string, parse as JSON first
191
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
192
+
193
+ const ctx = DeserializeContext.create();
194
+ const resultOrRef = User.deserializeWithContext(data, ctx);
195
+ if (PendingRef.is(resultOrRef)) {
196
+ return Result.err([
197
+ {
198
+ field: '_root',
199
+ message: 'User.deserialize: root cannot be a forward reference'
200
+ }
201
+ ]);
202
+ }
203
+ ctx.applyPatches();
204
+ if (opts?.freeze) {
205
+ ctx.freezeAll();
206
+ }
207
+ return Result.ok(resultOrRef);
208
+ } catch (e) {
209
+ if (e instanceof DeserializeError) {
210
+ return Result.err(e.errors);
211
+ }
212
+ const message = e instanceof Error ? e.message : String(e);
213
+ return Result.err([
214
+ {
215
+ field: '_root',
216
+ message
217
+ }
218
+ ]);
219
+ }
220
+ }
221
+
222
+ /** @internal */
223
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
224
+ if (value?.__ref !== undefined) {
225
+ return ctx.getOrDefer(value.__ref);
226
+ }
227
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
228
+ throw new DeserializeError([
229
+ {
230
+ field: '_root',
231
+ message: 'User.deserializeWithContext: expected an object'
232
+ }
233
+ ]);
234
+ }
235
+ const obj = value as Record<string, unknown>;
236
+ const errors: Array<{
237
+ field: string;
238
+ message: string;
239
+ }> = [];
240
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
241
+ for (const key of Object.keys(obj)) {
242
+ if (!knownKeys.has(key)) {
243
+ errors.push({
244
+ field: key,
245
+ message: 'unknown field'
246
+ });
247
+ }
248
+ }
249
+ if (!('id' in obj)) {
250
+ errors.push({
251
+ field: 'id',
252
+ message: 'missing required field'
253
+ });
254
+ }
255
+ if (!('email' in obj)) {
256
+ errors.push({
257
+ field: 'email',
258
+ message: 'missing required field'
259
+ });
260
+ }
261
+ if (errors.length > 0) {
262
+ throw new DeserializeError(errors);
263
+ }
264
+ const instance = Object.create(User.prototype) as User;
265
+ if (obj.__id !== undefined) {
266
+ ctx.register(obj.__id as number, instance);
267
+ }
268
+ ctx.trackForFreeze(instance);
269
+ {
270
+ const __raw_id = obj['id'] as number;
271
+ instance.id = __raw_id;
272
+ }
273
+ {
274
+ const __raw_email = obj['email'] as string;
275
+ instance.email = __raw_email;
276
+ }
277
+ if ('name' in obj && obj['name'] !== undefined) {
278
+ const __raw_name = obj['name'] as string;
279
+ instance.name = __raw_name;
280
+ } else {
281
+ instance.name = "guest";
282
+ }
283
+ if ('age' in obj && obj['age'] !== undefined) {
284
+ const __raw_age = obj['age'] as number;
285
+ instance.age = __raw_age;
286
+ }
287
+ if (errors.length > 0) {
288
+ throw new DeserializeError(errors);
289
+ }
290
+ return instance;
291
+ }
292
+
293
+ static validateField<K extends keyof User>(
294
+ field: K,
295
+ value: User[K]
296
+ ): Array<{
297
+ field: string;
298
+ message: string;
299
+ }> {
300
+ return [];
301
+ }
302
+
303
+ static validateFields(partial: Partial<User>): Array<{
304
+ field: string;
305
+ message: string;
306
+ }> {
307
+ return [];
308
+ }
309
+
310
+ static hasShape(obj: unknown): boolean {
311
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
312
+ return false;
313
+ }
314
+ const o = obj as Record<string, unknown>;
315
+ return 'id' in o && 'email' in o;
316
+ }
317
+
318
+ static is(obj: unknown): obj is User {
319
+ if (obj instanceof User) {
320
+ return true;
321
+ }
322
+ if (!User.hasShape(obj)) {
323
+ return false;
324
+ }
325
+ const result = User.deserialize(obj);
326
+ return Result.isOk(result);
327
+ }
328
+ }
329
+
330
+ // Usage:
331
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
332
+ if (Result.isOk(result)) {
333
+ const user = result.value;
334
+ } else {
335
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
336
+ }
337
+ ```
338
+
339
+ Generated output:
340
+
108
341
  ```typescript
109
- @derive(Deserialize)
110
- @serde(deny_unknown_fields)
342
+ import { DeserializeContext } from 'macroforge/serde';
343
+ import { DeserializeError } from 'macroforge/serde';
344
+ import type { DeserializeOptions } from 'macroforge/serde';
345
+ import { PendingRef } from 'macroforge/serde';
346
+
347
+ /** @serde({ denyUnknownFields: true }) */
111
348
  class User {
112
349
  id: number;
113
350
 
114
- @serde(validate(email, maxLength(255)))
115
351
  email: string;
116
352
 
117
- @serde(default = "guest")
118
353
  name: string;
119
354
 
120
- @serde(validate(positive))
121
355
  age?: number;
356
+
357
+ constructor(props: {
358
+ id: number;
359
+ email: string;
360
+ name?: string;
361
+ age?: number;
362
+ }) {
363
+ this.id = props.id;
364
+ this.email = props.email;
365
+ this.name = props.name as string;
366
+ this.age = props.age as number;
367
+ }
368
+
369
+ /**
370
+ * Deserializes input to an instance of this class.
371
+ * Automatically detects whether input is a JSON string or object.
372
+ * @param input - JSON string or object to deserialize
373
+ * @param opts - Optional deserialization options
374
+ * @returns Result containing the deserialized instance or validation errors
375
+ */
376
+ static deserialize(
377
+ input: unknown,
378
+ opts?: DeserializeOptions
379
+ ): Result<
380
+ User,
381
+ Array<{
382
+ field: string;
383
+ message: string;
384
+ }>
385
+ > {
386
+ try {
387
+ // Auto-detect: if string, parse as JSON first
388
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
389
+
390
+ const ctx = DeserializeContext.create();
391
+ const resultOrRef = User.deserializeWithContext(data, ctx);
392
+ if (PendingRef.is(resultOrRef)) {
393
+ return Result.err([
394
+ {
395
+ field: '_root',
396
+ message: 'User.deserialize: root cannot be a forward reference'
397
+ }
398
+ ]);
399
+ }
400
+ ctx.applyPatches();
401
+ if (opts?.freeze) {
402
+ ctx.freezeAll();
403
+ }
404
+ return Result.ok(resultOrRef);
405
+ } catch (e) {
406
+ if (e instanceof DeserializeError) {
407
+ return Result.err(e.errors);
408
+ }
409
+ const message = e instanceof Error ? e.message : String(e);
410
+ return Result.err([
411
+ {
412
+ field: '_root',
413
+ message
414
+ }
415
+ ]);
416
+ }
417
+ }
418
+
419
+ /** @internal */
420
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
421
+ if (value?.__ref !== undefined) {
422
+ return ctx.getOrDefer(value.__ref);
423
+ }
424
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
425
+ throw new DeserializeError([
426
+ {
427
+ field: '_root',
428
+ message: 'User.deserializeWithContext: expected an object'
429
+ }
430
+ ]);
431
+ }
432
+ const obj = value as Record<string, unknown>;
433
+ const errors: Array<{
434
+ field: string;
435
+ message: string;
436
+ }> = [];
437
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
438
+ for (const key of Object.keys(obj)) {
439
+ if (!knownKeys.has(key)) {
440
+ errors.push({
441
+ field: key,
442
+ message: 'unknown field'
443
+ });
444
+ }
445
+ }
446
+ if (!('id' in obj)) {
447
+ errors.push({
448
+ field: 'id',
449
+ message: 'missing required field'
450
+ });
451
+ }
452
+ if (!('email' in obj)) {
453
+ errors.push({
454
+ field: 'email',
455
+ message: 'missing required field'
456
+ });
457
+ }
458
+ if (errors.length > 0) {
459
+ throw new DeserializeError(errors);
460
+ }
461
+ const instance = Object.create(User.prototype) as User;
462
+ if (obj.__id !== undefined) {
463
+ ctx.register(obj.__id as number, instance);
464
+ }
465
+ ctx.trackForFreeze(instance);
466
+ {
467
+ const __raw_id = obj['id'] as number;
468
+ instance.id = __raw_id;
469
+ }
470
+ {
471
+ const __raw_email = obj['email'] as string;
472
+ instance.email = __raw_email;
473
+ }
474
+ if ('name' in obj && obj['name'] !== undefined) {
475
+ const __raw_name = obj['name'] as string;
476
+ instance.name = __raw_name;
477
+ } else {
478
+ instance.name = 'guest';
479
+ }
480
+ if ('age' in obj && obj['age'] !== undefined) {
481
+ const __raw_age = obj['age'] as number;
482
+ instance.age = __raw_age;
483
+ }
484
+ if (errors.length > 0) {
485
+ throw new DeserializeError(errors);
486
+ }
487
+ return instance;
488
+ }
489
+
490
+ static validateField<K extends keyof User>(
491
+ field: K,
492
+ value: User[K]
493
+ ): Array<{
494
+ field: string;
495
+ message: string;
496
+ }> {
497
+ return [];
498
+ }
499
+
500
+ static validateFields(partial: Partial<User>): Array<{
501
+ field: string;
502
+ message: string;
503
+ }> {
504
+ return [];
505
+ }
506
+
507
+ static hasShape(obj: unknown): boolean {
508
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
509
+ return false;
510
+ }
511
+ const o = obj as Record<string, unknown>;
512
+ return 'id' in o && 'email' in o;
513
+ }
514
+
515
+ static is(obj: unknown): obj is User {
516
+ if (obj instanceof User) {
517
+ return true;
518
+ }
519
+ if (!User.hasShape(obj)) {
520
+ return false;
521
+ }
522
+ const result = User.deserialize(obj);
523
+ return Result.isOk(result);
524
+ }
525
+ }
526
+
527
+ // Usage:
528
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
529
+ if (Result.isOk(result)) {
530
+ const user = result.value;
531
+ } else {
532
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
533
+ }
534
+ ```
535
+
536
+ Generated output:
537
+
538
+ ```typescript
539
+ import { DeserializeContext } from 'macroforge/serde';
540
+ import { DeserializeError } from 'macroforge/serde';
541
+ import type { DeserializeOptions } from 'macroforge/serde';
542
+ import { PendingRef } from 'macroforge/serde';
543
+
544
+ /** @serde({ denyUnknownFields: true }) */
545
+ class User {
546
+ id: number;
547
+
548
+ email: string;
549
+
550
+ name: string;
551
+
552
+ age?: number;
553
+
554
+ constructor(props: {
555
+ id: number;
556
+ email: string;
557
+ name?: string;
558
+ age?: number;
559
+ }) {
560
+ this.id = props.id;
561
+ this.email = props.email;
562
+ this.name = props.name as string;
563
+ this.age = props.age as number;
564
+ }
565
+
566
+ /**
567
+ * Deserializes input to an instance of this class.
568
+ * Automatically detects whether input is a JSON string or object.
569
+ * @param input - JSON string or object to deserialize
570
+ * @param opts - Optional deserialization options
571
+ * @returns Result containing the deserialized instance or validation errors
572
+ */
573
+ static deserialize(
574
+ input: unknown,
575
+ opts?: DeserializeOptions
576
+ ): Result<
577
+ User,
578
+ Array<{
579
+ field: string;
580
+ message: string;
581
+ }>
582
+ > {
583
+ try {
584
+ // Auto-detect: if string, parse as JSON first
585
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
586
+
587
+ const ctx = DeserializeContext.create();
588
+ const resultOrRef = User.deserializeWithContext(data, ctx);
589
+ if (PendingRef.is(resultOrRef)) {
590
+ return Result.err([
591
+ {
592
+ field: '_root',
593
+ message: 'User.deserialize: root cannot be a forward reference'
594
+ }
595
+ ]);
596
+ }
597
+ ctx.applyPatches();
598
+ if (opts?.freeze) {
599
+ ctx.freezeAll();
600
+ }
601
+ return Result.ok(resultOrRef);
602
+ } catch (e) {
603
+ if (e instanceof DeserializeError) {
604
+ return Result.err(e.errors);
605
+ }
606
+ const message = e instanceof Error ? e.message : String(e);
607
+ return Result.err([
608
+ {
609
+ field: '_root',
610
+ message
611
+ }
612
+ ]);
613
+ }
614
+ }
615
+
616
+ /** @internal */
617
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
618
+ if (value?.__ref !== undefined) {
619
+ return ctx.getOrDefer(value.__ref);
620
+ }
621
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
622
+ throw new DeserializeError([
623
+ {
624
+ field: '_root',
625
+ message: 'User.deserializeWithContext: expected an object'
626
+ }
627
+ ]);
628
+ }
629
+ const obj = value as Record<string, unknown>;
630
+ const errors: Array<{
631
+ field: string;
632
+ message: string;
633
+ }> = [];
634
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
635
+ for (const key of Object.keys(obj)) {
636
+ if (!knownKeys.has(key)) {
637
+ errors.push({
638
+ field: key,
639
+ message: 'unknown field'
640
+ });
641
+ }
642
+ }
643
+ if (!('id' in obj)) {
644
+ errors.push({
645
+ field: 'id',
646
+ message: 'missing required field'
647
+ });
648
+ }
649
+ if (!('email' in obj)) {
650
+ errors.push({
651
+ field: 'email',
652
+ message: 'missing required field'
653
+ });
654
+ }
655
+ if (errors.length > 0) {
656
+ throw new DeserializeError(errors);
657
+ }
658
+ const instance = Object.create(User.prototype) as User;
659
+ if (obj.__id !== undefined) {
660
+ ctx.register(obj.__id as number, instance);
661
+ }
662
+ ctx.trackForFreeze(instance);
663
+ {
664
+ const __raw_id = obj['id'] as number;
665
+ instance.id = __raw_id;
666
+ }
667
+ {
668
+ const __raw_email = obj['email'] as string;
669
+ instance.email = __raw_email;
670
+ }
671
+ if ('name' in obj && obj['name'] !== undefined) {
672
+ const __raw_name = obj['name'] as string;
673
+ instance.name = __raw_name;
674
+ } else {
675
+ instance.name = "guest";
676
+ }
677
+ if ('age' in obj && obj['age'] !== undefined) {
678
+ const __raw_age = obj['age'] as number;
679
+ instance.age = __raw_age;
680
+ }
681
+ if (errors.length > 0) {
682
+ throw new DeserializeError(errors);
683
+ }
684
+ return instance;
685
+ }
686
+
687
+ static validateField<K extends keyof User>(
688
+ field: K,
689
+ value: User[K]
690
+ ): Array<{
691
+ field: string;
692
+ message: string;
693
+ }> {
694
+ return [];
695
+ }
696
+
697
+ static validateFields(partial: Partial<User>): Array<{
698
+ field: string;
699
+ message: string;
700
+ }> {
701
+ return [];
702
+ }
703
+
704
+ static hasShape(obj: unknown): boolean {
705
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
706
+ return false;
707
+ }
708
+ const o = obj as Record<string, unknown>;
709
+ return 'id' in o && 'email' in o;
710
+ }
711
+
712
+ static is(obj: unknown): obj is User {
713
+ if (obj instanceof User) {
714
+ return true;
715
+ }
716
+ if (!User.hasShape(obj)) {
717
+ return false;
718
+ }
719
+ const result = User.deserialize(obj);
720
+ return Result.isOk(result);
721
+ }
722
+ }
723
+
724
+ // Usage:
725
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
726
+ if (Result.isOk(result)) {
727
+ const user = result.value;
728
+ } else {
729
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
730
+ }
731
+ ```
732
+
733
+ Generated output:
734
+
735
+ ```typescript
736
+ import { DeserializeContext } from 'macroforge/serde';
737
+ import { DeserializeError } from 'macroforge/serde';
738
+ import type { DeserializeOptions } from 'macroforge/serde';
739
+ import { PendingRef } from 'macroforge/serde';
740
+
741
+ /** @serde({ denyUnknownFields: true }) */
742
+ class User {
743
+ id: number;
744
+
745
+ email: string;
746
+
747
+ name: string;
748
+
749
+ age?: number;
750
+
751
+ constructor(props: {
752
+ id: number;
753
+ email: string;
754
+ name?: string;
755
+ age?: number;
756
+ }) {
757
+ this.id = props.id;
758
+ this.email = props.email;
759
+ this.name = props.name as string;
760
+ this.age = props.age as number;
761
+ }
762
+
763
+ /**
764
+ * Deserializes input to an instance of this class.
765
+ * Automatically detects whether input is a JSON string or object.
766
+ * @param input - JSON string or object to deserialize
767
+ * @param opts - Optional deserialization options
768
+ * @returns Result containing the deserialized instance or validation errors
769
+ */
770
+ static deserialize(
771
+ input: unknown,
772
+ opts?: DeserializeOptions
773
+ ): Result<
774
+ User,
775
+ Array<{
776
+ field: string;
777
+ message: string;
778
+ }>
779
+ > {
780
+ try {
781
+ // Auto-detect: if string, parse as JSON first
782
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
783
+
784
+ const ctx = DeserializeContext.create();
785
+ const resultOrRef = User.deserializeWithContext(data, ctx);
786
+ if (PendingRef.is(resultOrRef)) {
787
+ return Result.err([
788
+ {
789
+ field: '_root',
790
+ message: 'User.deserialize: root cannot be a forward reference'
791
+ }
792
+ ]);
793
+ }
794
+ ctx.applyPatches();
795
+ if (opts?.freeze) {
796
+ ctx.freezeAll();
797
+ }
798
+ return Result.ok(resultOrRef);
799
+ } catch (e) {
800
+ if (e instanceof DeserializeError) {
801
+ return Result.err(e.errors);
802
+ }
803
+ const message = e instanceof Error ? e.message : String(e);
804
+ return Result.err([
805
+ {
806
+ field: '_root',
807
+ message
808
+ }
809
+ ]);
810
+ }
811
+ }
812
+
813
+ /** @internal */
814
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
815
+ if (value?.__ref !== undefined) {
816
+ return ctx.getOrDefer(value.__ref);
817
+ }
818
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
819
+ throw new DeserializeError([
820
+ {
821
+ field: '_root',
822
+ message: 'User.deserializeWithContext: expected an object'
823
+ }
824
+ ]);
825
+ }
826
+ const obj = value as Record<string, unknown>;
827
+ const errors: Array<{
828
+ field: string;
829
+ message: string;
830
+ }> = [];
831
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
832
+ for (const key of Object.keys(obj)) {
833
+ if (!knownKeys.has(key)) {
834
+ errors.push({
835
+ field: key,
836
+ message: 'unknown field'
837
+ });
838
+ }
839
+ }
840
+ if (!('id' in obj)) {
841
+ errors.push({
842
+ field: 'id',
843
+ message: 'missing required field'
844
+ });
845
+ }
846
+ if (!('email' in obj)) {
847
+ errors.push({
848
+ field: 'email',
849
+ message: 'missing required field'
850
+ });
851
+ }
852
+ if (errors.length > 0) {
853
+ throw new DeserializeError(errors);
854
+ }
855
+ const instance = Object.create(User.prototype) as User;
856
+ if (obj.__id !== undefined) {
857
+ ctx.register(obj.__id as number, instance);
858
+ }
859
+ ctx.trackForFreeze(instance);
860
+ {
861
+ const __raw_id = obj['id'] as number;
862
+ instance.id = __raw_id;
863
+ }
864
+ {
865
+ const __raw_email = obj['email'] as string;
866
+ instance.email = __raw_email;
867
+ }
868
+ if ('name' in obj && obj['name'] !== undefined) {
869
+ const __raw_name = obj['name'] as string;
870
+ instance.name = __raw_name;
871
+ } else {
872
+ instance.name = 'guest';
873
+ }
874
+ if ('age' in obj && obj['age'] !== undefined) {
875
+ const __raw_age = obj['age'] as number;
876
+ instance.age = __raw_age;
877
+ }
878
+ if (errors.length > 0) {
879
+ throw new DeserializeError(errors);
880
+ }
881
+ return instance;
882
+ }
883
+
884
+ static validateField<K extends keyof User>(
885
+ field: K,
886
+ value: User[K]
887
+ ): Array<{
888
+ field: string;
889
+ message: string;
890
+ }> {
891
+ return [];
892
+ }
893
+
894
+ static validateFields(partial: Partial<User>): Array<{
895
+ field: string;
896
+ message: string;
897
+ }> {
898
+ return [];
899
+ }
900
+
901
+ static hasShape(obj: unknown): boolean {
902
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
903
+ return false;
904
+ }
905
+ const o = obj as Record<string, unknown>;
906
+ return 'id' in o && 'email' in o;
907
+ }
908
+
909
+ static is(obj: unknown): obj is User {
910
+ if (obj instanceof User) {
911
+ return true;
912
+ }
913
+ if (!User.hasShape(obj)) {
914
+ return false;
915
+ }
916
+ const result = User.deserialize(obj);
917
+ return Result.isOk(result);
918
+ }
919
+ }
920
+
921
+ // Usage:
922
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
923
+ if (Result.isOk(result)) {
924
+ const user = result.value;
925
+ } else {
926
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
927
+ }
928
+ ```
929
+
930
+ Generated output:
931
+
932
+ ```typescript
933
+ import { DeserializeContext } from 'macroforge/serde';
934
+ import { DeserializeError } from 'macroforge/serde';
935
+ import type { DeserializeOptions } from 'macroforge/serde';
936
+ import { PendingRef } from 'macroforge/serde';
937
+
938
+ /** @serde({ denyUnknownFields: true }) */
939
+ class User {
940
+ id: number;
941
+
942
+ email: string;
943
+
944
+ name: string;
945
+
946
+ age?: number;
947
+
948
+ constructor(props: {
949
+ id: number;
950
+ email: string;
951
+ name?: string;
952
+ age?: number;
953
+ }) {
954
+ this.id = props.id;
955
+ this.email = props.email;
956
+ this.name = props.name as string;
957
+ this.age = props.age as number;
958
+ }
959
+
960
+ /**
961
+ * Deserializes input to an instance of this class.
962
+ * Automatically detects whether input is a JSON string or object.
963
+ * @param input - JSON string or object to deserialize
964
+ * @param opts - Optional deserialization options
965
+ * @returns Result containing the deserialized instance or validation errors
966
+ */
967
+ static deserialize(
968
+ input: unknown,
969
+ opts?: DeserializeOptions
970
+ ): Result<
971
+ User,
972
+ Array<{
973
+ field: string;
974
+ message: string;
975
+ }>
976
+ > {
977
+ try {
978
+ // Auto-detect: if string, parse as JSON first
979
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
980
+
981
+ const ctx = DeserializeContext.create();
982
+ const resultOrRef = User.deserializeWithContext(data, ctx);
983
+ if (PendingRef.is(resultOrRef)) {
984
+ return Result.err([
985
+ {
986
+ field: '_root',
987
+ message: 'User.deserialize: root cannot be a forward reference'
988
+ }
989
+ ]);
990
+ }
991
+ ctx.applyPatches();
992
+ if (opts?.freeze) {
993
+ ctx.freezeAll();
994
+ }
995
+ return Result.ok(resultOrRef);
996
+ } catch (e) {
997
+ if (e instanceof DeserializeError) {
998
+ return Result.err(e.errors);
999
+ }
1000
+ const message = e instanceof Error ? e.message : String(e);
1001
+ return Result.err([
1002
+ {
1003
+ field: '_root',
1004
+ message
1005
+ }
1006
+ ]);
1007
+ }
1008
+ }
1009
+
1010
+ /** @internal */
1011
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
1012
+ if (value?.__ref !== undefined) {
1013
+ return ctx.getOrDefer(value.__ref);
1014
+ }
1015
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1016
+ throw new DeserializeError([
1017
+ {
1018
+ field: '_root',
1019
+ message: 'User.deserializeWithContext: expected an object'
1020
+ }
1021
+ ]);
1022
+ }
1023
+ const obj = value as Record<string, unknown>;
1024
+ const errors: Array<{
1025
+ field: string;
1026
+ message: string;
1027
+ }> = [];
1028
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
1029
+ for (const key of Object.keys(obj)) {
1030
+ if (!knownKeys.has(key)) {
1031
+ errors.push({
1032
+ field: key,
1033
+ message: 'unknown field'
1034
+ });
1035
+ }
1036
+ }
1037
+ if (!('id' in obj)) {
1038
+ errors.push({
1039
+ field: 'id',
1040
+ message: 'missing required field'
1041
+ });
1042
+ }
1043
+ if (!('email' in obj)) {
1044
+ errors.push({
1045
+ field: 'email',
1046
+ message: 'missing required field'
1047
+ });
1048
+ }
1049
+ if (errors.length > 0) {
1050
+ throw new DeserializeError(errors);
1051
+ }
1052
+ const instance = Object.create(User.prototype) as User;
1053
+ if (obj.__id !== undefined) {
1054
+ ctx.register(obj.__id as number, instance);
1055
+ }
1056
+ ctx.trackForFreeze(instance);
1057
+ {
1058
+ const __raw_id = obj['id'] as number;
1059
+ instance.id = __raw_id;
1060
+ }
1061
+ {
1062
+ const __raw_email = obj['email'] as string;
1063
+ instance.email = __raw_email;
1064
+ }
1065
+ if ('name' in obj && obj['name'] !== undefined) {
1066
+ const __raw_name = obj['name'] as string;
1067
+ instance.name = __raw_name;
1068
+ } else {
1069
+ instance.name = "guest";
1070
+ }
1071
+ if ('age' in obj && obj['age'] !== undefined) {
1072
+ const __raw_age = obj['age'] as number;
1073
+ instance.age = __raw_age;
1074
+ }
1075
+ if (errors.length > 0) {
1076
+ throw new DeserializeError(errors);
1077
+ }
1078
+ return instance;
1079
+ }
1080
+
1081
+ static validateField<K extends keyof User>(
1082
+ field: K,
1083
+ value: User[K]
1084
+ ): Array<{
1085
+ field: string;
1086
+ message: string;
1087
+ }> {
1088
+ return [];
1089
+ }
1090
+
1091
+ static validateFields(partial: Partial<User>): Array<{
1092
+ field: string;
1093
+ message: string;
1094
+ }> {
1095
+ return [];
1096
+ }
1097
+
1098
+ static hasShape(obj: unknown): boolean {
1099
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
1100
+ return false;
1101
+ }
1102
+ const o = obj as Record<string, unknown>;
1103
+ return 'id' in o && 'email' in o;
1104
+ }
1105
+
1106
+ static is(obj: unknown): obj is User {
1107
+ if (obj instanceof User) {
1108
+ return true;
1109
+ }
1110
+ if (!User.hasShape(obj)) {
1111
+ return false;
1112
+ }
1113
+ const result = User.deserialize(obj);
1114
+ return Result.isOk(result);
1115
+ }
1116
+ }
1117
+
1118
+ // Usage:
1119
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
1120
+ if (Result.isOk(result)) {
1121
+ const user = result.value;
1122
+ } else {
1123
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
1124
+ }
1125
+ ```
1126
+
1127
+ Generated output:
1128
+
1129
+ ```typescript
1130
+ import { DeserializeContext } from 'macroforge/serde';
1131
+ import { DeserializeError } from 'macroforge/serde';
1132
+ import type { DeserializeOptions } from 'macroforge/serde';
1133
+ import { PendingRef } from 'macroforge/serde';
1134
+
1135
+ /** @serde({ denyUnknownFields: true }) */
1136
+ class User {
1137
+ id: number;
1138
+
1139
+ email: string;
1140
+
1141
+ name: string;
1142
+
1143
+ age?: number;
1144
+
1145
+ constructor(props: {
1146
+ id: number;
1147
+ email: string;
1148
+ name?: string;
1149
+ age?: number;
1150
+ }) {
1151
+ this.id = props.id;
1152
+ this.email = props.email;
1153
+ this.name = props.name as string;
1154
+ this.age = props.age as number;
1155
+ }
1156
+
1157
+ /**
1158
+ * Deserializes input to an instance of this class.
1159
+ * Automatically detects whether input is a JSON string or object.
1160
+ * @param input - JSON string or object to deserialize
1161
+ * @param opts - Optional deserialization options
1162
+ * @returns Result containing the deserialized instance or validation errors
1163
+ */
1164
+ static deserialize(
1165
+ input: unknown,
1166
+ opts?: DeserializeOptions
1167
+ ): Result<
1168
+ User,
1169
+ Array<{
1170
+ field: string;
1171
+ message: string;
1172
+ }>
1173
+ > {
1174
+ try {
1175
+ // Auto-detect: if string, parse as JSON first
1176
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
1177
+
1178
+ const ctx = DeserializeContext.create();
1179
+ const resultOrRef = User.deserializeWithContext(data, ctx);
1180
+ if (PendingRef.is(resultOrRef)) {
1181
+ return Result.err([
1182
+ {
1183
+ field: '_root',
1184
+ message: 'User.deserialize: root cannot be a forward reference'
1185
+ }
1186
+ ]);
1187
+ }
1188
+ ctx.applyPatches();
1189
+ if (opts?.freeze) {
1190
+ ctx.freezeAll();
1191
+ }
1192
+ return Result.ok(resultOrRef);
1193
+ } catch (e) {
1194
+ if (e instanceof DeserializeError) {
1195
+ return Result.err(e.errors);
1196
+ }
1197
+ const message = e instanceof Error ? e.message : String(e);
1198
+ return Result.err([
1199
+ {
1200
+ field: '_root',
1201
+ message
1202
+ }
1203
+ ]);
1204
+ }
1205
+ }
1206
+
1207
+ /** @internal */
1208
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
1209
+ if (value?.__ref !== undefined) {
1210
+ return ctx.getOrDefer(value.__ref);
1211
+ }
1212
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1213
+ throw new DeserializeError([
1214
+ {
1215
+ field: '_root',
1216
+ message: 'User.deserializeWithContext: expected an object'
1217
+ }
1218
+ ]);
1219
+ }
1220
+ const obj = value as Record<string, unknown>;
1221
+ const errors: Array<{
1222
+ field: string;
1223
+ message: string;
1224
+ }> = [];
1225
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
1226
+ for (const key of Object.keys(obj)) {
1227
+ if (!knownKeys.has(key)) {
1228
+ errors.push({
1229
+ field: key,
1230
+ message: 'unknown field'
1231
+ });
1232
+ }
1233
+ }
1234
+ if (!('id' in obj)) {
1235
+ errors.push({
1236
+ field: 'id',
1237
+ message: 'missing required field'
1238
+ });
1239
+ }
1240
+ if (!('email' in obj)) {
1241
+ errors.push({
1242
+ field: 'email',
1243
+ message: 'missing required field'
1244
+ });
1245
+ }
1246
+ if (errors.length > 0) {
1247
+ throw new DeserializeError(errors);
1248
+ }
1249
+ const instance = Object.create(User.prototype) as User;
1250
+ if (obj.__id !== undefined) {
1251
+ ctx.register(obj.__id as number, instance);
1252
+ }
1253
+ ctx.trackForFreeze(instance);
1254
+ {
1255
+ const __raw_id = obj['id'] as number;
1256
+ instance.id = __raw_id;
1257
+ }
1258
+ {
1259
+ const __raw_email = obj['email'] as string;
1260
+ instance.email = __raw_email;
1261
+ }
1262
+ if ('name' in obj && obj['name'] !== undefined) {
1263
+ const __raw_name = obj['name'] as string;
1264
+ instance.name = __raw_name;
1265
+ } else {
1266
+ instance.name = 'guest';
1267
+ }
1268
+ if ('age' in obj && obj['age'] !== undefined) {
1269
+ const __raw_age = obj['age'] as number;
1270
+ instance.age = __raw_age;
1271
+ }
1272
+ if (errors.length > 0) {
1273
+ throw new DeserializeError(errors);
1274
+ }
1275
+ return instance;
1276
+ }
1277
+
1278
+ static validateField<K extends keyof User>(
1279
+ field: K,
1280
+ value: User[K]
1281
+ ): Array<{
1282
+ field: string;
1283
+ message: string;
1284
+ }> {
1285
+ return [];
1286
+ }
1287
+
1288
+ static validateFields(partial: Partial<User>): Array<{
1289
+ field: string;
1290
+ message: string;
1291
+ }> {
1292
+ return [];
1293
+ }
1294
+
1295
+ static hasShape(obj: unknown): boolean {
1296
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
1297
+ return false;
1298
+ }
1299
+ const o = obj as Record<string, unknown>;
1300
+ return 'id' in o && 'email' in o;
1301
+ }
1302
+
1303
+ static is(obj: unknown): obj is User {
1304
+ if (obj instanceof User) {
1305
+ return true;
1306
+ }
1307
+ if (!User.hasShape(obj)) {
1308
+ return false;
1309
+ }
1310
+ const result = User.deserialize(obj);
1311
+ return Result.isOk(result);
1312
+ }
1313
+ }
1314
+
1315
+ // Usage:
1316
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
1317
+ if (Result.isOk(result)) {
1318
+ const user = result.value;
1319
+ } else {
1320
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
1321
+ }
1322
+ ```
1323
+
1324
+ Generated output:
1325
+
1326
+ ```typescript
1327
+ import { DeserializeContext } from 'macroforge/serde';
1328
+ import { DeserializeError } from 'macroforge/serde';
1329
+ import type { DeserializeOptions } from 'macroforge/serde';
1330
+ import { PendingRef } from 'macroforge/serde';
1331
+
1332
+ /** @serde({ denyUnknownFields: true }) */
1333
+ class User {
1334
+ id: number;
1335
+
1336
+ email: string;
1337
+
1338
+ name: string;
1339
+
1340
+ age?: number;
1341
+
1342
+ constructor(props: {
1343
+ id: number;
1344
+ email: string;
1345
+ name?: string;
1346
+ age?: number;
1347
+ }) {
1348
+ this.id = props.id;
1349
+ this.email = props.email;
1350
+ this.name = props.name as string;
1351
+ this.age = props.age as number;
1352
+ }
1353
+
1354
+ /**
1355
+ * Deserializes input to an instance of this class.
1356
+ * Automatically detects whether input is a JSON string or object.
1357
+ * @param input - JSON string or object to deserialize
1358
+ * @param opts - Optional deserialization options
1359
+ * @returns Result containing the deserialized instance or validation errors
1360
+ */
1361
+ static deserialize(
1362
+ input: unknown,
1363
+ opts?: DeserializeOptions
1364
+ ): Result<
1365
+ User,
1366
+ Array<{
1367
+ field: string;
1368
+ message: string;
1369
+ }>
1370
+ > {
1371
+ try {
1372
+ // Auto-detect: if string, parse as JSON first
1373
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
1374
+
1375
+ const ctx = DeserializeContext.create();
1376
+ const resultOrRef = User.deserializeWithContext(data, ctx);
1377
+ if (PendingRef.is(resultOrRef)) {
1378
+ return Result.err([
1379
+ {
1380
+ field: '_root',
1381
+ message: 'User.deserialize: root cannot be a forward reference'
1382
+ }
1383
+ ]);
1384
+ }
1385
+ ctx.applyPatches();
1386
+ if (opts?.freeze) {
1387
+ ctx.freezeAll();
1388
+ }
1389
+ return Result.ok(resultOrRef);
1390
+ } catch (e) {
1391
+ if (e instanceof DeserializeError) {
1392
+ return Result.err(e.errors);
1393
+ }
1394
+ const message = e instanceof Error ? e.message : String(e);
1395
+ return Result.err([
1396
+ {
1397
+ field: '_root',
1398
+ message
1399
+ }
1400
+ ]);
1401
+ }
1402
+ }
1403
+
1404
+ /** @internal */
1405
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
1406
+ if (value?.__ref !== undefined) {
1407
+ return ctx.getOrDefer(value.__ref);
1408
+ }
1409
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1410
+ throw new DeserializeError([
1411
+ {
1412
+ field: '_root',
1413
+ message: 'User.deserializeWithContext: expected an object'
1414
+ }
1415
+ ]);
1416
+ }
1417
+ const obj = value as Record<string, unknown>;
1418
+ const errors: Array<{
1419
+ field: string;
1420
+ message: string;
1421
+ }> = [];
1422
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
1423
+ for (const key of Object.keys(obj)) {
1424
+ if (!knownKeys.has(key)) {
1425
+ errors.push({
1426
+ field: key,
1427
+ message: 'unknown field'
1428
+ });
1429
+ }
1430
+ }
1431
+ if (!('id' in obj)) {
1432
+ errors.push({
1433
+ field: 'id',
1434
+ message: 'missing required field'
1435
+ });
1436
+ }
1437
+ if (!('email' in obj)) {
1438
+ errors.push({
1439
+ field: 'email',
1440
+ message: 'missing required field'
1441
+ });
1442
+ }
1443
+ if (errors.length > 0) {
1444
+ throw new DeserializeError(errors);
1445
+ }
1446
+ const instance = Object.create(User.prototype) as User;
1447
+ if (obj.__id !== undefined) {
1448
+ ctx.register(obj.__id as number, instance);
1449
+ }
1450
+ ctx.trackForFreeze(instance);
1451
+ {
1452
+ const __raw_id = obj['id'] as number;
1453
+ instance.id = __raw_id;
1454
+ }
1455
+ {
1456
+ const __raw_email = obj['email'] as string;
1457
+ instance.email = __raw_email;
1458
+ }
1459
+ if ('name' in obj && obj['name'] !== undefined) {
1460
+ const __raw_name = obj['name'] as string;
1461
+ instance.name = __raw_name;
1462
+ } else {
1463
+ instance.name = 'guest';
1464
+ }
1465
+ if ('age' in obj && obj['age'] !== undefined) {
1466
+ const __raw_age = obj['age'] as number;
1467
+ instance.age = __raw_age;
1468
+ }
1469
+ if (errors.length > 0) {
1470
+ throw new DeserializeError(errors);
1471
+ }
1472
+ return instance;
1473
+ }
1474
+
1475
+ static validateField<K extends keyof User>(
1476
+ field: K,
1477
+ value: User[K]
1478
+ ): Array<{
1479
+ field: string;
1480
+ message: string;
1481
+ }> {
1482
+ return [];
1483
+ }
1484
+
1485
+ static validateFields(partial: Partial<User>): Array<{
1486
+ field: string;
1487
+ message: string;
1488
+ }> {
1489
+ return [];
1490
+ }
1491
+
1492
+ static hasShape(obj: unknown): boolean {
1493
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
1494
+ return false;
1495
+ }
1496
+ const o = obj as Record<string, unknown>;
1497
+ return 'id' in o && 'email' in o;
1498
+ }
1499
+
1500
+ static is(obj: unknown): obj is User {
1501
+ if (obj instanceof User) {
1502
+ return true;
1503
+ }
1504
+ if (!User.hasShape(obj)) {
1505
+ return false;
1506
+ }
1507
+ const result = User.deserialize(obj);
1508
+ return Result.isOk(result);
1509
+ }
1510
+ }
1511
+
1512
+ // Usage:
1513
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
1514
+ if (Result.isOk(result)) {
1515
+ const user = result.value;
1516
+ } else {
1517
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
1518
+ }
1519
+ ```
1520
+
1521
+ Generated output:
1522
+
1523
+ ```typescript
1524
+ import { DeserializeContext } from 'macroforge/serde';
1525
+ import { DeserializeError } from 'macroforge/serde';
1526
+ import type { DeserializeOptions } from 'macroforge/serde';
1527
+ import { PendingRef } from 'macroforge/serde';
1528
+
1529
+ /** @serde({ denyUnknownFields: true }) */
1530
+ class User {
1531
+ id: number;
1532
+
1533
+ email: string;
1534
+
1535
+ name: string;
1536
+
1537
+ age?: number;
1538
+
1539
+ constructor(props: {
1540
+ id: number;
1541
+ email: string;
1542
+ name?: string;
1543
+ age?: number;
1544
+ }) {
1545
+ this.id = props.id;
1546
+ this.email = props.email;
1547
+ this.name = props.name as string;
1548
+ this.age = props.age as number;
1549
+ }
1550
+
1551
+ /**
1552
+ * Deserializes input to an instance of this class.
1553
+ * Automatically detects whether input is a JSON string or object.
1554
+ * @param input - JSON string or object to deserialize
1555
+ * @param opts - Optional deserialization options
1556
+ * @returns Result containing the deserialized instance or validation errors
1557
+ */
1558
+ static deserialize(
1559
+ input: unknown,
1560
+ opts?: DeserializeOptions
1561
+ ): Result<
1562
+ User,
1563
+ Array<{
1564
+ field: string;
1565
+ message: string;
1566
+ }>
1567
+ > {
1568
+ try {
1569
+ // Auto-detect: if string, parse as JSON first
1570
+ const data = typeof input === 'string' ? JSON.parse(input) : input;
1571
+
1572
+ const ctx = DeserializeContext.create();
1573
+ const resultOrRef = User.deserializeWithContext(data, ctx);
1574
+ if (PendingRef.is(resultOrRef)) {
1575
+ return Result.err([
1576
+ {
1577
+ field: '_root',
1578
+ message: 'User.deserialize: root cannot be a forward reference'
1579
+ }
1580
+ ]);
1581
+ }
1582
+ ctx.applyPatches();
1583
+ if (opts?.freeze) {
1584
+ ctx.freezeAll();
1585
+ }
1586
+ return Result.ok(resultOrRef);
1587
+ } catch (e) {
1588
+ if (e instanceof DeserializeError) {
1589
+ return Result.err(e.errors);
1590
+ }
1591
+ const message = e instanceof Error ? e.message : String(e);
1592
+ return Result.err([
1593
+ {
1594
+ field: '_root',
1595
+ message
1596
+ }
1597
+ ]);
1598
+ }
1599
+ }
1600
+
1601
+ /** @internal */
1602
+ static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
1603
+ if (value?.__ref !== undefined) {
1604
+ return ctx.getOrDefer(value.__ref);
1605
+ }
1606
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1607
+ throw new DeserializeError([
1608
+ {
1609
+ field: '_root',
1610
+ message: 'User.deserializeWithContext: expected an object'
1611
+ }
1612
+ ]);
1613
+ }
1614
+ const obj = value as Record<string, unknown>;
1615
+ const errors: Array<{
1616
+ field: string;
1617
+ message: string;
1618
+ }> = [];
1619
+ const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
1620
+ for (const key of Object.keys(obj)) {
1621
+ if (!knownKeys.has(key)) {
1622
+ errors.push({
1623
+ field: key,
1624
+ message: 'unknown field'
1625
+ });
1626
+ }
1627
+ }
1628
+ if (!('id' in obj)) {
1629
+ errors.push({
1630
+ field: 'id',
1631
+ message: 'missing required field'
1632
+ });
1633
+ }
1634
+ if (!('email' in obj)) {
1635
+ errors.push({
1636
+ field: 'email',
1637
+ message: 'missing required field'
1638
+ });
1639
+ }
1640
+ if (errors.length > 0) {
1641
+ throw new DeserializeError(errors);
1642
+ }
1643
+ const instance = Object.create(User.prototype) as User;
1644
+ if (obj.__id !== undefined) {
1645
+ ctx.register(obj.__id as number, instance);
1646
+ }
1647
+ ctx.trackForFreeze(instance);
1648
+ {
1649
+ const __raw_id = obj['id'] as number;
1650
+ instance.id = __raw_id;
1651
+ }
1652
+ {
1653
+ const __raw_email = obj['email'] as string;
1654
+ instance.email = __raw_email;
1655
+ }
1656
+ if ('name' in obj && obj['name'] !== undefined) {
1657
+ const __raw_name = obj['name'] as string;
1658
+ instance.name = __raw_name;
1659
+ } else {
1660
+ instance.name = 'guest';
1661
+ }
1662
+ if ('age' in obj && obj['age'] !== undefined) {
1663
+ const __raw_age = obj['age'] as number;
1664
+ instance.age = __raw_age;
1665
+ }
1666
+ if (errors.length > 0) {
1667
+ throw new DeserializeError(errors);
1668
+ }
1669
+ return instance;
1670
+ }
1671
+
1672
+ static validateField<K extends keyof User>(
1673
+ field: K,
1674
+ value: User[K]
1675
+ ): Array<{
1676
+ field: string;
1677
+ message: string;
1678
+ }> {
1679
+ return [];
1680
+ }
1681
+
1682
+ static validateFields(partial: Partial<User>): Array<{
1683
+ field: string;
1684
+ message: string;
1685
+ }> {
1686
+ return [];
1687
+ }
1688
+
1689
+ static hasShape(obj: unknown): boolean {
1690
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
1691
+ return false;
1692
+ }
1693
+ const o = obj as Record<string, unknown>;
1694
+ return 'id' in o && 'email' in o;
1695
+ }
1696
+
1697
+ static is(obj: unknown): obj is User {
1698
+ if (obj instanceof User) {
1699
+ return true;
1700
+ }
1701
+ if (!User.hasShape(obj)) {
1702
+ return false;
1703
+ }
1704
+ const result = User.deserialize(obj);
1705
+ return Result.isOk(result);
1706
+ }
122
1707
  }
123
1708
 
124
1709
  // Usage:
125
- const result = User.fromStringifiedJSON('{"id":1,"email":"test@example.com"}');
1710
+ const result = User.deserialize('{"id":1,"email":"test@example.com"}');
126
1711
  if (Result.isOk(result)) {
127
1712
  const user = result.value;
128
1713
  } else {
129
- console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
1714
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
130
1715
  }
131
1716
  ```
132
1717