@macroforge/mcp-server 0.1.34 → 0.1.36

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.
@@ -1,1042 +1,137 @@
1
1
  # Deserialize
2
- *The `Deserialize` macro generates JSON deserialization methods with **cycle and forward-reference support**, plus comprehensive runtime validation. This enables safe parsing of complex JSON structures including circular references.*
3
- ## Basic Usage
4
- **Before:**
5
- ```
6
- /** @derive(Deserialize) */
7
- class User {
8
- name: string;
9
- age: number;
10
- createdAt: Date;
11
- }
12
- ```
13
- **After:**
14
- ```
15
- import { Result } from 'macroforge/utils';
16
- import { DeserializeContext } from 'macroforge/serde';
17
- import { DeserializeError } from 'macroforge/serde';
18
- import type { DeserializeOptions } from 'macroforge/serde';
19
- import { PendingRef } from 'macroforge/serde';
20
-
21
- class User {
22
- name: string;
23
- age: number;
24
- createdAt: Date;
25
2
 
26
- constructor(props: {
27
- name: string;
28
- age: number;
29
- createdAt: Date;
30
- }) {
31
- this.name = props.name;
32
- this.age = props.age;
33
- this.createdAt = props.createdAt;
34
- }
3
+ The `Deserialize` macro generates JSON deserialization methods with **cycle and
4
+ forward-reference support**, plus comprehensive runtime validation. This enables
5
+ safe parsing of complex JSON structures including circular references.
35
6
 
36
- static fromStringifiedJSON(
37
- json: string,
38
- opts?: DeserializeOptions
39
- ): Result<
40
- User,
41
- Array<{
42
- field: string;
43
- message: string;
44
- }>
45
- > {
46
- try {
47
- const raw = JSON.parse(json);
48
- return User.fromObject(raw, opts);
49
- } catch (e) {
50
- if (e instanceof DeserializeError) {
51
- return Result.err(e.errors);
52
- }
53
- const message = e instanceof Error ? e.message : String(e);
54
- return Result.err([
55
- {
56
- field: '_root',
57
- message
58
- }
59
- ]);
60
- }
61
- }
7
+ ## Generated Output
62
8
 
63
- static fromObject(
64
- obj: unknown,
65
- opts?: DeserializeOptions
66
- ): Result<
67
- User,
68
- Array<{
69
- field: string;
70
- message: string;
71
- }>
72
- > {
73
- try {
74
- const ctx = DeserializeContext.create();
75
- const resultOrRef = User.__deserialize(obj, ctx);
76
- if (PendingRef.is(resultOrRef)) {
77
- return Result.err([
78
- {
79
- field: '_root',
80
- message: 'User.fromObject: root cannot be a forward reference'
81
- }
82
- ]);
83
- }
84
- ctx.applyPatches();
85
- if (opts?.freeze) {
86
- ctx.freezeAll();
87
- }
88
- return Result.ok(resultOrRef);
89
- } catch (e) {
90
- if (e instanceof DeserializeError) {
91
- return Result.err(e.errors);
92
- }
93
- const message = e instanceof Error ? e.message : String(e);
94
- return Result.err([
95
- {
96
- field: '_root',
97
- message
98
- }
99
- ]);
100
- }
101
- }
9
+ | Type | Generated Code | Description |
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 |
102
15
 
103
- static __deserialize(value: any, ctx: DeserializeContext): User | PendingRef {
104
- if (value?.__ref !== undefined) {
105
- return ctx.getOrDefer(value.__ref);
106
- }
107
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
108
- throw new DeserializeError([
109
- {
110
- field: '_root',
111
- message: 'User.__deserialize: expected an object'
112
- }
113
- ]);
114
- }
115
- const obj = value as Record<string, unknown>;
116
- const errors: Array<{
117
- field: string;
118
- message: string;
119
- }> = [];
120
- if (!('name' in obj)) {
121
- errors.push({
122
- field: 'name',
123
- message: 'missing required field'
124
- });
125
- }
126
- if (!('age' in obj)) {
127
- errors.push({
128
- field: 'age',
129
- message: 'missing required field'
130
- });
131
- }
132
- if (!('createdAt' in obj)) {
133
- errors.push({
134
- field: 'createdAt',
135
- message: 'missing required field'
136
- });
137
- }
138
- if (errors.length > 0) {
139
- throw new DeserializeError(errors);
140
- }
141
- const instance = Object.create(User.prototype) as User;
142
- if (obj.__id !== undefined) {
143
- ctx.register(obj.__id as number, instance);
144
- }
145
- ctx.trackForFreeze(instance);
146
- {
147
- const __raw_name = obj['name'] as string;
148
- instance.name = __raw_name;
149
- }
150
- {
151
- const __raw_age = obj['age'] as number;
152
- instance.age = __raw_age;
153
- }
154
- {
155
- const __raw_createdAt = obj['createdAt'] as Date;
156
- {
157
- const __dateVal =
158
- typeof __raw_createdAt === 'string'
159
- ? new Date(__raw_createdAt)
160
- : (__raw_createdAt as Date);
161
- instance.createdAt = __dateVal;
162
- }
163
- }
164
- if (errors.length > 0) {
165
- throw new DeserializeError(errors);
166
- }
167
- return instance;
168
- }
16
+ ## Configuration
169
17
 
170
- static validateField<K extends keyof User>(
171
- field: K,
172
- value: User[K]
173
- ): Array<{
174
- field: string;
175
- message: string;
176
- }> {
177
- return [];
178
- }
18
+ The `functionNamingStyle` option in `macroforge.json` controls naming:
19
+ - `"suffix"` (default): Suffixes with type name (e.g., `fromStringifiedJSONMyType`)
20
+ - `"prefix"`: Prefixes with type name (e.g., `myTypeFromStringifiedJSON`)
21
+ - `"generic"`: Uses TypeScript generics (e.g., `fromStringifiedJSON<T extends MyType>`)
22
+ - `"namespace"`: Legacy namespace wrapping
179
23
 
180
- static validateFields(partial: Partial<User>): Array<{
181
- field: string;
182
- message: string;
183
- }> {
184
- return [];
185
- }
186
- }
187
- ``` ```
188
- const json = '{"name":"Alice","age":30,"createdAt":"2024-01-15T10:30:00.000Z"}';
189
- const user = User.fromJSON(JSON.parse(json));
24
+ ## Return Type
190
25
 
191
- console.log(user.name); // "Alice"
192
- console.log(user.age); // 30
193
- console.log(user.createdAt instanceof Date); // true
194
- ``` ## Runtime Validation
195
- Deserialize validates the input data and throws descriptive errors:
196
- **Source:**
197
- ```
198
- /** @derive(Deserialize) */
199
- class User {
200
- name: string;
201
- email: string;
202
- }
203
- ``` ```
204
- // Missing required field
205
- User.fromJSON({ name: "Alice" });
206
- // Error: User.fromJSON: missing required field "email"
26
+ All public deserialization methods return `Result<T, Array<{ field: string; message: string }>>`:
207
27
 
208
- // Wrong type
209
- User.fromJSON("not an object");
210
- // Error: User.fromJSON: expected an object, got string
28
+ - `Result.ok(value)` - Successfully deserialized value
29
+ - `Result.err(errors)` - Array of validation errors with field names and messages
211
30
 
212
- // Array instead of object
213
- User.fromJSON([1, 2, 3]);
214
- // Error: User.fromJSON: expected an object, got array
215
- ``` ## Automatic Type Conversion
216
- Deserialize automatically converts JSON types to their TypeScript equivalents:
217
- | JSON Type | TypeScript Type | Conversion |
218
- | --- | --- | --- |
219
- | string/number/boolean | `string`/`number`/`boolean` | Direct assignment |
220
- | ISO string | `Date` | `new Date(string)` |
221
- | array | `T[]` | Maps items with auto-detection |
222
- | object | `Map<K, V>` | `new Map(Object.entries())` |
223
- | array | `Set<T>` | `new Set(array)` |
224
- | object | Nested class | Calls `fromJSON()` if available |
225
- ## Serde Options
226
- Use the `@serde` decorator to customize deserialization:
227
- ### Renaming Fields
228
- **Before:**
229
- ```
230
- /** @derive(Deserialize) */
231
- class User {
232
- /** @serde({ rename: "user_id" }) */
233
- id: string;
31
+ ## Cycle/Forward-Reference Support
234
32
 
235
- /** @serde({ rename: "full_name" }) */
236
- name: string;
237
- }
238
- ```
239
- **After:**
240
- ```
241
- import { Result } from 'macroforge/utils';
242
- import { DeserializeContext } from 'macroforge/serde';
243
- import { DeserializeError } from 'macroforge/serde';
244
- import type { DeserializeOptions } from 'macroforge/serde';
245
- import { PendingRef } from 'macroforge/serde';
33
+ Uses deferred patching to handle references:
246
34
 
247
- class User {
248
- id: string;
35
+ 1. When encountering `{ "__ref": id }`, returns a `PendingRef` marker
36
+ 2. Continues deserializing other fields
37
+ 3. After all objects are created, `ctx.applyPatches()` resolves all pending references
249
38
 
250
- name: string;
39
+ References only apply to object-shaped, serializable values. The generator avoids probing for
40
+ `__ref` on primitive-like fields (including literal unions and `T | null` where `T` is primitive-like),
41
+ and it parses `Date` / `Date | null` from ISO strings without treating them as references.
251
42
 
252
- constructor(props: {
253
- id: string;
254
- name: string;
255
- }) {
256
- this.id = props.id;
257
- this.name = props.name;
258
- }
43
+ ## Validation
259
44
 
260
- static fromStringifiedJSON(
261
- json: string,
262
- opts?: DeserializeOptions
263
- ): Result<
264
- User,
265
- Array<{
266
- field: string;
267
- message: string;
268
- }>
269
- > {
270
- try {
271
- const raw = JSON.parse(json);
272
- return User.fromObject(raw, opts);
273
- } catch (e) {
274
- if (e instanceof DeserializeError) {
275
- return Result.err(e.errors);
276
- }
277
- const message = e instanceof Error ? e.message : String(e);
278
- return Result.err([
279
- {
280
- field: '_root',
281
- message
282
- }
283
- ]);
284
- }
285
- }
45
+ The macro supports 30+ validators via `@serde(validate(...))`:
286
46
 
287
- static fromObject(
288
- obj: unknown,
289
- opts?: DeserializeOptions
290
- ): Result<
291
- User,
292
- Array<{
293
- field: string;
294
- message: string;
295
- }>
296
- > {
297
- try {
298
- const ctx = DeserializeContext.create();
299
- const resultOrRef = User.__deserialize(obj, ctx);
300
- if (PendingRef.is(resultOrRef)) {
301
- return Result.err([
302
- {
303
- field: '_root',
304
- message: 'User.fromObject: root cannot be a forward reference'
305
- }
306
- ]);
307
- }
308
- ctx.applyPatches();
309
- if (opts?.freeze) {
310
- ctx.freezeAll();
311
- }
312
- return Result.ok(resultOrRef);
313
- } catch (e) {
314
- if (e instanceof DeserializeError) {
315
- return Result.err(e.errors);
316
- }
317
- const message = e instanceof Error ? e.message : String(e);
318
- return Result.err([
319
- {
320
- field: '_root',
321
- message
322
- }
323
- ]);
324
- }
325
- }
47
+ ### String Validators
48
+ - `email`, `url`, `uuid` - Format validation
49
+ - `minLength(n)`, `maxLength(n)`, `length(n)` - Length constraints
50
+ - `pattern("regex")` - Regular expression matching
51
+ - `nonEmpty`, `trimmed`, `lowercase`, `uppercase` - String properties
326
52
 
327
- static __deserialize(value: any, ctx: DeserializeContext): User | PendingRef {
328
- if (value?.__ref !== undefined) {
329
- return ctx.getOrDefer(value.__ref);
330
- }
331
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
332
- throw new DeserializeError([
333
- {
334
- field: '_root',
335
- message: 'User.__deserialize: expected an object'
336
- }
337
- ]);
338
- }
339
- const obj = value as Record<string, unknown>;
340
- const errors: Array<{
341
- field: string;
342
- message: string;
343
- }> = [];
344
- if (!('user_id' in obj)) {
345
- errors.push({
346
- field: 'user_id',
347
- message: 'missing required field'
348
- });
349
- }
350
- if (!('full_name' in obj)) {
351
- errors.push({
352
- field: 'full_name',
353
- message: 'missing required field'
354
- });
355
- }
356
- if (errors.length > 0) {
357
- throw new DeserializeError(errors);
358
- }
359
- const instance = Object.create(User.prototype) as User;
360
- if (obj.__id !== undefined) {
361
- ctx.register(obj.__id as number, instance);
362
- }
363
- ctx.trackForFreeze(instance);
364
- {
365
- const __raw_id = obj['user_id'] as string;
366
- instance.id = __raw_id;
367
- }
368
- {
369
- const __raw_name = obj['full_name'] as string;
370
- instance.name = __raw_name;
371
- }
372
- if (errors.length > 0) {
373
- throw new DeserializeError(errors);
374
- }
375
- return instance;
376
- }
53
+ ### Number Validators
54
+ - `gt(n)`, `gte(n)`, `lt(n)`, `lte(n)`, `between(min, max)` - Range checks
55
+ - `int`, `positive`, `nonNegative`, `finite` - Number properties
377
56
 
378
- static validateField<K extends keyof User>(
379
- field: K,
380
- value: User[K]
381
- ): Array<{
382
- field: string;
383
- message: string;
384
- }> {
385
- return [];
386
- }
57
+ ### Array Validators
58
+ - `minItems(n)`, `maxItems(n)`, `itemsCount(n)` - Collection size
387
59
 
388
- static validateFields(partial: Partial<User>): Array<{
389
- field: string;
390
- message: string;
391
- }> {
392
- return [];
393
- }
394
- }
395
- ``` ```
396
- const user = User.fromJSON({ user_id: "123", full_name: "Alice" });
397
- console.log(user.id); // "123"
398
- console.log(user.name); // "Alice"
399
- ``` ### Default Values
400
- **Before:**
401
- ```
402
- /** @derive(Deserialize) */
403
- class Config {
404
- host: string;
60
+ ### Date Validators
61
+ - `validDate`, `afterDate("ISO")`, `beforeDate("ISO")` - Date validation
405
62
 
406
- /** @serde({ default: "3000" }) */
407
- port: string;
63
+ ## Field-Level Options
408
64
 
409
- /** @serde({ default: "false" }) */
410
- debug: boolean;
411
- }
412
- ```
413
- **After:**
414
- ```
415
- import { Result } from 'macroforge/utils';
416
- import { DeserializeContext } from 'macroforge/serde';
417
- import { DeserializeError } from 'macroforge/serde';
418
- import type { DeserializeOptions } from 'macroforge/serde';
419
- import { PendingRef } from 'macroforge/serde';
65
+ The `@serde` decorator supports:
420
66
 
421
- class Config {
422
- host: string;
67
+ - `skip` / `skip_deserializing` - Exclude field from deserialization
68
+ - `rename = "jsonKey"` - Read from different JSON property
69
+ - `default` / `default = expr` - Use default value if missing
70
+ - `flatten` - Read fields from parent object level
71
+ - `validate(...)` - Apply validators
423
72
 
424
- port: string;
73
+ ## Container-Level Options
425
74
 
426
- debug: boolean;
75
+ - `deny_unknown_fields` - Error on unrecognized JSON properties
76
+ - `rename_all = "camelCase"` - Apply naming convention to all fields
427
77
 
428
- constructor(props: {
429
- host: string;
430
- port?: string;
431
- debug?: boolean;
432
- }) {
433
- this.host = props.host;
434
- this.port = props.port as string;
435
- this.debug = props.debug as boolean;
436
- }
78
+ ## Union Type Deserialization
437
79
 
438
- static fromStringifiedJSON(
439
- json: string,
440
- opts?: DeserializeOptions
441
- ): Result<
442
- Config,
443
- Array<{
444
- field: string;
445
- message: string;
446
- }>
447
- > {
448
- try {
449
- const raw = JSON.parse(json);
450
- return Config.fromObject(raw, opts);
451
- } catch (e) {
452
- if (e instanceof DeserializeError) {
453
- return Result.err(e.errors);
454
- }
455
- const message = e instanceof Error ? e.message : String(e);
456
- return Result.err([
457
- {
458
- field: '_root',
459
- message
460
- }
461
- ]);
462
- }
463
- }
80
+ Union types are deserialized based on their member types:
464
81
 
465
- static fromObject(
466
- obj: unknown,
467
- opts?: DeserializeOptions
468
- ): Result<
469
- Config,
470
- Array<{
471
- field: string;
472
- message: string;
473
- }>
474
- > {
475
- try {
476
- const ctx = DeserializeContext.create();
477
- const resultOrRef = Config.__deserialize(obj, ctx);
478
- if (PendingRef.is(resultOrRef)) {
479
- return Result.err([
480
- {
481
- field: '_root',
482
- message: 'Config.fromObject: root cannot be a forward reference'
483
- }
484
- ]);
485
- }
486
- ctx.applyPatches();
487
- if (opts?.freeze) {
488
- ctx.freezeAll();
489
- }
490
- return Result.ok(resultOrRef);
491
- } catch (e) {
492
- if (e instanceof DeserializeError) {
493
- return Result.err(e.errors);
494
- }
495
- const message = e instanceof Error ? e.message : String(e);
496
- return Result.err([
497
- {
498
- field: '_root',
499
- message
500
- }
501
- ]);
502
- }
503
- }
82
+ ### Literal Unions
83
+ For unions of literal values (`"A" | "B" | 123`), the value is validated against
84
+ the allowed literals directly.
504
85
 
505
- static __deserialize(value: any, ctx: DeserializeContext): Config | PendingRef {
506
- if (value?.__ref !== undefined) {
507
- return ctx.getOrDefer(value.__ref);
508
- }
509
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
510
- throw new DeserializeError([
511
- {
512
- field: '_root',
513
- message: 'Config.__deserialize: expected an object'
514
- }
515
- ]);
516
- }
517
- const obj = value as Record<string, unknown>;
518
- const errors: Array<{
519
- field: string;
520
- message: string;
521
- }> = [];
522
- if (!('host' in obj)) {
523
- errors.push({
524
- field: 'host',
525
- message: 'missing required field'
526
- });
527
- }
528
- if (errors.length > 0) {
529
- throw new DeserializeError(errors);
530
- }
531
- const instance = Object.create(Config.prototype) as Config;
532
- if (obj.__id !== undefined) {
533
- ctx.register(obj.__id as number, instance);
534
- }
535
- ctx.trackForFreeze(instance);
536
- {
537
- const __raw_host = obj['host'] as string;
538
- instance.host = __raw_host;
539
- }
540
- if ('port' in obj && obj['port'] !== undefined) {
541
- const __raw_port = obj['port'] as string;
542
- instance.port = __raw_port;
543
- } else {
544
- instance.port = 3000;
545
- }
546
- if ('debug' in obj && obj['debug'] !== undefined) {
547
- const __raw_debug = obj['debug'] as boolean;
548
- instance.debug = __raw_debug;
549
- } else {
550
- instance.debug = false;
551
- }
552
- if (errors.length > 0) {
553
- throw new DeserializeError(errors);
554
- }
555
- return instance;
556
- }
86
+ ### Primitive Unions
87
+ For unions containing primitive types (`string | number`), the deserializer uses
88
+ `typeof` checks to validate the value type. No `__type` discriminator is needed.
557
89
 
558
- static validateField<K extends keyof Config>(
559
- field: K,
560
- value: Config[K]
561
- ): Array<{
562
- field: string;
563
- message: string;
564
- }> {
565
- return [];
566
- }
90
+ ### Class/Interface Unions
91
+ 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.
567
93
 
568
- static validateFields(partial: Partial<Config>): Array<{
569
- field: string;
570
- message: string;
571
- }> {
572
- return [];
573
- }
574
- }
575
- ``` ```
576
- const config = Config.fromJSON({ host: "localhost" });
577
- console.log(config.port); // "3000"
578
- console.log(config.debug); // false
579
- ``` ### Skipping Fields
580
- **Source:**
581
- ```
582
- /** @derive(Deserialize) */
583
- class User {
584
- name: string;
585
- email: string;
94
+ ### Generic Type Parameters
95
+ For generic unions like `type Result<T> = T | Error`, the generic type parameter `T`
96
+ is passed through as-is since its concrete type is only known at the call site.
586
97
 
587
- /** @serde({ skip: true }) */
588
- cachedData: unknown;
98
+ ### Mixed Unions
99
+ Mixed unions (e.g., `string | Date | User`) check in order:
100
+ 1. Literal values
101
+ 2. Primitives (via `typeof`)
102
+ 3. Date (via `instanceof` or ISO string parsing)
103
+ 4. Serializable types (via `__type` dispatch)
104
+ 5. Generic type parameters (pass-through)
589
105
 
590
- /** @serde({ skip_deserializing: true }) */
591
- computedField: string;
592
- }
593
- ``` **skip vs skip_deserializing Use `skip: true` to exclude from both serialization and deserialization.
594
- Use `skip_deserializing: true` to only skip during deserialization. ### Deny Unknown Fields
595
- ****Source:**
596
- ```
597
- /** @derive(Deserialize) */
598
- /** @serde({ deny_unknown_fields: true }) */
599
- class StrictUser {
600
- name: string;
601
- email: string;
602
- }
603
- ``` ```
604
- // This will throw an error
605
- StrictUser.fromJSON({ name: "Alice", email: "a@b.com", extra: "field" });
606
- // Error: StrictUser.fromJSON: unknown field "extra"
607
- ``` ### Flatten Nested Objects
608
- **Source:**
609
- ```
610
- /** @derive(Deserialize) */
611
- class Address {
612
- city: string;
613
- zip: string;
614
- }
106
+ ## Example
615
107
 
616
- /** @derive(Deserialize) */
108
+ ```typescript
109
+ @derive(Deserialize)
110
+ @serde(deny_unknown_fields)
617
111
  class User {
618
- name: string;
619
-
620
- /** @serde({ flatten: true }) */
621
- address: Address;
622
- }
623
- ``` ```
624
- // Flat JSON structure
625
- const user = User.fromJSON({
626
- name: "Alice",
627
- city: "NYC",
628
- zip: "10001"
629
- });
630
- console.log(user.address.city); // "NYC"
631
- ``` ## All Options
632
- ### Container Options (on class/interface)
633
- | Option | Type | Description |
634
- | --- | --- | --- |
635
- | `rename_all` | `string` | Apply naming convention to all fields |
636
- | `deny_unknown_fields` | `boolean` | Throw error if JSON has unknown keys |
637
- ### Field Options (on properties)
638
- | Option | Type | Description |
639
- | --- | --- | --- |
640
- | `rename` | `string` | Use a different JSON key |
641
- | `skip` | `boolean` | Exclude from serialization and deserialization |
642
- | `skip_deserializing` | `boolean` | Exclude from deserialization only |
643
- | `default` | `boolean | string` | Use TypeScript default or custom expression if missing |
644
- | `flatten` | `boolean` | Merge nested object fields from parent |
645
- ## Interface Support
646
- Deserialize also works with interfaces. For interfaces, a namespace is generated with `is` (type guard) and `fromJSON` functions:
647
- **Before:**
648
- ```
649
- /** @derive(Deserialize) */
650
- interface ApiResponse {
651
- status: number;
652
- message: string;
653
- timestamp: Date;
654
- }
655
- ```
656
- **After:**
657
- ```
658
- import { Result } from 'macroforge/utils';
659
- import { DeserializeContext } from 'macroforge/serde';
660
- import { DeserializeError } from 'macroforge/serde';
661
- import type { DeserializeOptions } from 'macroforge/serde';
662
- import { PendingRef } from 'macroforge/serde';
663
-
664
- interface ApiResponse {
665
- status: number;
666
- message: string;
667
- timestamp: Date;
668
- }
669
-
670
- export namespace ApiResponse {
671
- export function fromStringifiedJSON(
672
- json: string,
673
- opts?: DeserializeOptions
674
- ): Result<ApiResponse, Array<{ field: string; message: string }>> {
675
- try {
676
- const raw = JSON.parse(json);
677
- return fromObject(raw, opts);
678
- } catch (e) {
679
- if (e instanceof DeserializeError) {
680
- return Result.err(e.errors);
681
- }
682
- const message = e instanceof Error ? e.message : String(e);
683
- return Result.err([{ field: '_root', message }]);
684
- }
685
- }
686
- export function fromObject(
687
- obj: unknown,
688
- opts?: DeserializeOptions
689
- ): Result<ApiResponse, Array<{ field: string; message: string }>> {
690
- try {
691
- const ctx = DeserializeContext.create();
692
- const resultOrRef = __deserialize(obj, ctx);
693
- if (PendingRef.is(resultOrRef)) {
694
- return Result.err([
695
- {
696
- field: '_root',
697
- message: 'ApiResponse.fromObject: root cannot be a forward reference'
698
- }
699
- ]);
700
- }
701
- ctx.applyPatches();
702
- if (opts?.freeze) {
703
- ctx.freezeAll();
704
- }
705
- return Result.ok(resultOrRef);
706
- } catch (e) {
707
- if (e instanceof DeserializeError) {
708
- return Result.err(e.errors);
709
- }
710
- const message = e instanceof Error ? e.message : String(e);
711
- return Result.err([{ field: '_root', message }]);
712
- }
713
- }
714
- export function __deserialize(value: any, ctx: DeserializeContext): ApiResponse | PendingRef {
715
- if (value?.__ref !== undefined) {
716
- return ctx.getOrDefer(value.__ref);
717
- }
718
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
719
- throw new DeserializeError([
720
- { field: '_root', message: 'ApiResponse.__deserialize: expected an object' }
721
- ]);
722
- }
723
- const obj = value as Record<string, unknown>;
724
- const errors: Array<{ field: string; message: string }> = [];
725
- if (!('status' in obj)) {
726
- errors.push({ field: 'status', message: 'missing required field' });
727
- }
728
- if (!('message' in obj)) {
729
- errors.push({ field: 'message', message: 'missing required field' });
730
- }
731
- if (!('timestamp' in obj)) {
732
- errors.push({ field: 'timestamp', message: 'missing required field' });
733
- }
734
- if (errors.length > 0) {
735
- throw new DeserializeError(errors);
736
- }
737
- const instance: any = {};
738
- if (obj.__id !== undefined) {
739
- ctx.register(obj.__id as number, instance);
740
- }
741
- ctx.trackForFreeze(instance);
742
- {
743
- const __raw_status = obj['status'] as number;
744
- instance.status = __raw_status;
745
- }
746
- {
747
- const __raw_message = obj['message'] as string;
748
- instance.message = __raw_message;
749
- }
750
- {
751
- const __raw_timestamp = obj['timestamp'] as Date;
752
- {
753
- const __dateVal =
754
- typeof __raw_timestamp === 'string'
755
- ? new Date(__raw_timestamp)
756
- : (__raw_timestamp as Date);
757
- instance.timestamp = __dateVal;
758
- }
759
- }
760
- if (errors.length > 0) {
761
- throw new DeserializeError(errors);
762
- }
763
- return instance as ApiResponse;
764
- }
765
- export function validateField<K extends keyof ApiResponse>(
766
- field: K,
767
- value: ApiResponse[K]
768
- ): Array<{ field: string; message: string }> {
769
- return [];
770
- }
771
- export function validateFields(
772
- partial: Partial<ApiResponse>
773
- ): Array<{ field: string; message: string }> {
774
- return [];
775
- }
776
- }
777
- ``` ```
778
- const json = { status: 200, message: "OK", timestamp: "2024-01-15T10:30:00.000Z" };
779
-
780
- // Type guard
781
- if (ApiResponse.is(json)) {
782
- console.log(json.status); // TypeScript knows this is ApiResponse
783
- }
784
-
785
- // Deserialize with validation
786
- const response = ApiResponse.fromJSON(json);
787
- console.log(response.timestamp instanceof Date); // true
788
- ``` ## Enum Support
789
- Deserialize also works with enums. The `fromJSON` function validates that the input matches one of the enum values:
790
- **Before:**
791
- ```
792
- /** @derive(Deserialize) */
793
- enum Status {
794
- Active = 'active',
795
- Inactive = 'inactive',
796
- Pending = 'pending'
797
- }
798
- ```
799
- **After:**
800
- ```
801
- import { DeserializeContext } from 'macroforge/serde';
802
-
803
- enum Status {
804
- Active = 'active',
805
- Inactive = 'inactive',
806
- Pending = 'pending'
807
- }
112
+ id: number;
808
113
 
809
- export namespace Status {
810
- export function fromStringifiedJSON(json: string): Status {
811
- const data = JSON.parse(json);
812
- return __deserialize(data);
813
- }
814
- export function __deserialize(data: unknown): Status {
815
- for (const key of Object.keys(Status)) {
816
- const enumValue = Status[key as keyof typeof Status];
817
- if (enumValue === data) {
818
- return data as Status;
819
- }
820
- }
821
- throw new Error('Invalid Status value: ' + JSON.stringify(data));
822
- }
823
- }
824
- ``` ```
825
- const status = Status.fromJSON("active");
826
- console.log(status); // Status.Active
827
-
828
- // Invalid values throw an error
829
- try {
830
- Status.fromJSON("invalid");
831
- } catch (e) {
832
- console.log(e.message); // "Invalid Status value: invalid"
833
- }
834
- ``` Works with numeric enums too:
835
- **Source:**
836
- ```
837
- /** @derive(Deserialize) */
838
- enum Priority {
839
- Low = 1,
840
- Medium = 2,
841
- High = 3,
842
- }
843
- ``` ```
844
- const priority = Priority.fromJSON(3);
845
- console.log(priority); // Priority.High
846
- ``` ## Type Alias Support
847
- Deserialize works with type aliases. For object types, validation and type conversion is applied:
848
- **Before:**
849
- ```
850
- /** @derive(Deserialize) */
851
- type UserProfile = {
852
- id: string;
853
- name: string;
854
- createdAt: Date;
855
- };
856
- ```
857
- **After:**
858
- ```
859
- import { Result } from 'macroforge/utils';
860
- import { DeserializeContext } from 'macroforge/serde';
861
- import { DeserializeError } from 'macroforge/serde';
862
- import type { DeserializeOptions } from 'macroforge/serde';
863
- import { PendingRef } from 'macroforge/serde';
114
+ @serde(validate(email, maxLength(255)))
115
+ email: string;
864
116
 
865
- type UserProfile = {
866
- id: string;
117
+ @serde(default = "guest")
867
118
  name: string;
868
- createdAt: Date;
869
- };
870
119
 
871
- export namespace UserProfile {
872
- export function fromStringifiedJSON(
873
- json: string,
874
- opts?: DeserializeOptions
875
- ): Result<UserProfile, Array<{ field: string; message: string }>> {
876
- try {
877
- const raw = JSON.parse(json);
878
- return fromObject(raw, opts);
879
- } catch (e) {
880
- if (e instanceof DeserializeError) {
881
- return Result.err(e.errors);
882
- }
883
- const message = e instanceof Error ? e.message : String(e);
884
- return Result.err([{ field: '_root', message }]);
885
- }
886
- }
887
- export function fromObject(
888
- obj: unknown,
889
- opts?: DeserializeOptions
890
- ): Result<UserProfile, Array<{ field: string; message: string }>> {
891
- try {
892
- const ctx = DeserializeContext.create();
893
- const resultOrRef = __deserialize(obj, ctx);
894
- if (PendingRef.is(resultOrRef)) {
895
- return Result.err([
896
- {
897
- field: '_root',
898
- message: 'UserProfile.fromObject: root cannot be a forward reference'
899
- }
900
- ]);
901
- }
902
- ctx.applyPatches();
903
- if (opts?.freeze) {
904
- ctx.freezeAll();
905
- }
906
- return Result.ok(resultOrRef);
907
- } catch (e) {
908
- if (e instanceof DeserializeError) {
909
- return Result.err(e.errors);
910
- }
911
- const message = e instanceof Error ? e.message : String(e);
912
- return Result.err([{ field: '_root', message }]);
913
- }
914
- }
915
- export function __deserialize(value: any, ctx: DeserializeContext): UserProfile | PendingRef {
916
- if (value?.__ref !== undefined) {
917
- return ctx.getOrDefer(value.__ref) as UserProfile | PendingRef;
918
- }
919
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
920
- throw new DeserializeError([
921
- { field: '_root', message: 'UserProfile.__deserialize: expected an object' }
922
- ]);
923
- }
924
- const obj = value as Record<string, unknown>;
925
- const errors: Array<{ field: string; message: string }> = [];
926
- if (!('id' in obj)) {
927
- errors.push({ field: 'id', message: 'missing required field' });
928
- }
929
- if (!('name' in obj)) {
930
- errors.push({ field: 'name', message: 'missing required field' });
931
- }
932
- if (!('createdAt' in obj)) {
933
- errors.push({ field: 'createdAt', message: 'missing required field' });
934
- }
935
- if (errors.length > 0) {
936
- throw new DeserializeError(errors);
937
- }
938
- const instance: any = {};
939
- if (obj.__id !== undefined) {
940
- ctx.register(obj.__id as number, instance);
941
- }
942
- ctx.trackForFreeze(instance);
943
- {
944
- const __raw_id = obj['id'] as string;
945
- instance.id = __raw_id;
946
- }
947
- {
948
- const __raw_name = obj['name'] as string;
949
- instance.name = __raw_name;
950
- }
951
- {
952
- const __raw_createdAt = obj['createdAt'] as Date;
953
- {
954
- const __dateVal =
955
- typeof __raw_createdAt === 'string'
956
- ? new Date(__raw_createdAt)
957
- : (__raw_createdAt as Date);
958
- instance.createdAt = __dateVal;
959
- }
960
- }
961
- if (errors.length > 0) {
962
- throw new DeserializeError(errors);
963
- }
964
- return instance as UserProfile;
965
- }
966
- export function validateField<K extends keyof UserProfile>(
967
- field: K,
968
- value: UserProfile[K]
969
- ): Array<{ field: string; message: string }> {
970
- return [];
971
- }
972
- export function validateFields(
973
- partial: Partial<UserProfile>
974
- ): Array<{ field: string; message: string }> {
975
- return [];
976
- }
120
+ @serde(validate(positive))
121
+ age?: number;
977
122
  }
978
- ``` ```
979
- const json = {
980
- id: "123",
981
- name: "Alice",
982
- createdAt: "2024-01-15T00:00:00.000Z"
983
- };
984
123
 
985
- const profile = UserProfile.fromJSON(json);
986
- console.log(profile.createdAt instanceof Date); // true
987
- ``` For union types, basic validation is applied:
988
- **Source:**
989
- ```
990
- /** @derive(Deserialize) */
991
- type ApiStatus = "loading" | "success" | "error";
992
- ``` ```
993
- const status = ApiStatus.fromJSON("success");
994
- console.log(status); // "success"
995
- ``` ## Combining with Serialize
996
- Use both Serialize and Deserialize for complete JSON round-trip support:
997
- **Source:**
998
- ```
999
- /** @derive(Serialize, Deserialize) */
1000
- /** @serde({ rename_all: "camelCase" }) */
1001
- class UserProfile {
1002
- user_name: string;
1003
- created_at: Date;
1004
- is_active: boolean;
124
+ // Usage:
125
+ const result = User.fromStringifiedJSON('{"id":1,"email":"test@example.com"}');
126
+ if (Result.isOk(result)) {
127
+ const user = result.value;
128
+ } else {
129
+ console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
1005
130
  }
1006
- ``` ```
1007
- // Create and serialize
1008
- const profile = new UserProfile();
1009
- profile.user_name = "Alice";
1010
- profile.created_at = new Date();
1011
- profile.is_active = true;
1012
-
1013
- const json = JSON.stringify(profile);
1014
- // {"userName":"Alice","createdAt":"2024-...","isActive":true}
1015
-
1016
- // Deserialize back
1017
- const restored = UserProfile.fromJSON(JSON.parse(json));
1018
- console.log(restored.user_name); // "Alice"
1019
- console.log(restored.created_at instanceof Date); // true
1020
- ``` ## Error Handling
1021
- Handle deserialization errors gracefully:
1022
- **Source:**
1023
131
  ```
1024
- /** @derive(Deserialize) */
1025
- class User {
1026
- name: string;
1027
- email: string;
1028
- }
1029
- ``` ```
1030
- function parseUser(json: unknown): User | null {
1031
- try {
1032
- return User.fromJSON(json);
1033
- } catch (error) {
1034
- console.error("Failed to parse user:", error.message);
1035
- return null;
1036
- }
1037
- }
1038
132
 
1039
- const user = parseUser({ name: "Alice" });
1040
- // Logs: Failed to parse user: User.fromJSON: missing required field "email"
1041
- // Returns: null
1042
- ```
133
+ ## Required Imports
134
+
135
+ The generated code automatically imports:
136
+ - `Result` from `macroforge/utils`
137
+ - `DeserializeContext`, `DeserializeError`, `PendingRef` from `macroforge/serde`