@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.
- package/docs/builtin-macros/clone.md +42 -183
- package/docs/builtin-macros/debug.md +33 -239
- package/docs/builtin-macros/default.md +78 -257
- package/docs/builtin-macros/deserialize.md +94 -999
- package/docs/builtin-macros/hash.md +62 -260
- package/docs/builtin-macros/ord.md +70 -251
- package/docs/builtin-macros/partial-eq.md +55 -262
- package/docs/builtin-macros/partial-ord.md +69 -272
- package/docs/builtin-macros/serialize.md +63 -382
- package/docs/concepts/derive-system.md +1 -4
- package/package.json +2 -2
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
id: string;
|
|
254
|
-
name: string;
|
|
255
|
-
}) {
|
|
256
|
-
this.id = props.id;
|
|
257
|
-
this.name = props.name;
|
|
258
|
-
}
|
|
43
|
+
## Validation
|
|
259
44
|
|
|
260
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
407
|
-
port: string;
|
|
63
|
+
## Field-Level Options
|
|
408
64
|
|
|
409
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
73
|
+
## Container-Level Options
|
|
425
74
|
|
|
426
|
-
|
|
75
|
+
- `deny_unknown_fields` - Error on unrecognized JSON properties
|
|
76
|
+
- `rename_all = "camelCase"` - Apply naming convention to all fields
|
|
427
77
|
|
|
428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
```typescript
|
|
109
|
+
@derive(Deserialize)
|
|
110
|
+
@serde(deny_unknown_fields)
|
|
617
111
|
class User {
|
|
618
|
-
|
|
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
|
-
|
|
810
|
-
|
|
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
|
-
|
|
866
|
-
id: string;
|
|
117
|
+
@serde(default = "guest")
|
|
867
118
|
name: string;
|
|
868
|
-
createdAt: Date;
|
|
869
|
-
};
|
|
870
119
|
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
133
|
+
## Required Imports
|
|
134
|
+
|
|
135
|
+
The generated code automatically imports:
|
|
136
|
+
- `Result` from `macroforge/utils`
|
|
137
|
+
- `DeserializeContext`, `DeserializeError`, `PendingRef` from `macroforge/serde`
|