@macroforge/mcp-server 0.1.32 → 0.1.34
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/README.md +68 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/docs-loader.d.ts +133 -5
- package/dist/tools/docs-loader.d.ts.map +1 -1
- package/dist/tools/docs-loader.js +131 -15
- package/dist/tools/docs-loader.js.map +1 -1
- package/dist/tools/index.d.ts +48 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +163 -14
- package/dist/tools/index.js.map +1 -1
- package/docs/api/api-overview.md +24 -46
- package/docs/api/expand-sync.md +24 -51
- package/docs/api/native-plugin.md +24 -56
- package/docs/api/position-mapper.md +34 -76
- package/docs/api/transform-sync.md +27 -59
- package/docs/builtin-macros/clone.md +150 -68
- package/docs/builtin-macros/debug.md +216 -81
- package/docs/builtin-macros/default.md +234 -91
- package/docs/builtin-macros/deserialize.md +891 -166
- package/docs/builtin-macros/hash.md +238 -82
- package/docs/builtin-macros/macros-overview.md +42 -103
- package/docs/builtin-macros/ord.md +205 -92
- package/docs/builtin-macros/partial-eq.md +178 -97
- package/docs/builtin-macros/partial-ord.md +209 -98
- package/docs/builtin-macros/serialize.md +326 -137
- package/docs/concepts/architecture.md +40 -99
- package/docs/concepts/derive-system.md +132 -125
- package/docs/concepts/how-macros-work.md +52 -84
- package/docs/custom-macros/custom-overview.md +17 -39
- package/docs/custom-macros/rust-setup.md +22 -55
- package/docs/custom-macros/ts-macro-derive.md +43 -107
- package/docs/custom-macros/ts-quote.md +177 -507
- package/docs/getting-started/first-macro.md +108 -33
- package/docs/getting-started/installation.md +32 -73
- package/docs/integration/cli.md +70 -156
- package/docs/integration/configuration.md +32 -75
- package/docs/integration/integration-overview.md +16 -55
- package/docs/integration/mcp-server.md +30 -69
- package/docs/integration/svelte-preprocessor.md +60 -83
- package/docs/integration/typescript-plugin.md +32 -74
- package/docs/integration/vite-plugin.md +30 -79
- package/docs/language-servers/ls-overview.md +22 -46
- package/docs/language-servers/svelte.md +30 -69
- package/docs/language-servers/zed.md +34 -72
- package/docs/roadmap/roadmap.md +54 -130
- package/docs/sections.json +3 -262
- package/package.json +2 -2
|
@@ -1,31 +1,206 @@
|
|
|
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';
|
|
2
20
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
21
|
+
class User {
|
|
22
|
+
name: string;
|
|
23
|
+
age: number;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
|
|
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
|
+
}
|
|
35
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
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
|
+
}
|
|
102
|
+
|
|
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
|
+
}
|
|
169
|
+
|
|
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
|
+
}
|
|
179
|
+
|
|
180
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
181
|
+
field: string;
|
|
182
|
+
message: string;
|
|
183
|
+
}> {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
``` ```
|
|
10
188
|
const json = '{"name":"Alice","age":30,"createdAt":"2024-01-15T10:30:00.000Z"}';
|
|
11
189
|
const user = User.fromJSON(JSON.parse(json));
|
|
12
190
|
|
|
13
191
|
console.log(user.name); // "Alice"
|
|
14
192
|
console.log(user.age); // 30
|
|
15
193
|
console.log(user.createdAt instanceof Date); // true
|
|
194
|
+
``` ## Runtime Validation
|
|
195
|
+
Deserialize validates the input data and throws descriptive errors:
|
|
196
|
+
**Source:**
|
|
16
197
|
```
|
|
17
|
-
|
|
18
|
-
## Runtime Validation
|
|
19
|
-
|
|
20
|
-
Deserialize validates the input data and throws descriptive errors:
|
|
21
|
-
|
|
22
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
198
|
+
/** @derive(Deserialize) */
|
|
23
199
|
class User {
|
|
24
200
|
name: string;
|
|
25
201
|
email: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
202
|
+
}
|
|
203
|
+
``` ```
|
|
29
204
|
// Missing required field
|
|
30
205
|
User.fromJSON({ name: "Alice" });
|
|
31
206
|
// Error: User.fromJSON: missing required field "email"
|
|
@@ -37,63 +212,374 @@ User.fromJSON("not an object");
|
|
|
37
212
|
// Array instead of object
|
|
38
213
|
User.fromJSON([1, 2, 3]);
|
|
39
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:**
|
|
40
229
|
```
|
|
230
|
+
/** @derive(Deserialize) */
|
|
231
|
+
class User {
|
|
232
|
+
/** @serde({ rename: "user_id" }) */
|
|
233
|
+
id: string;
|
|
41
234
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| array
|
|
55
|
-
| `T[]`
|
|
56
|
-
| Maps items with auto-detection
|
|
57
|
-
|
|
58
|
-
| object
|
|
59
|
-
| `Map<K, V>`
|
|
60
|
-
| `new Map(Object.entries())`
|
|
61
|
-
|
|
62
|
-
| array
|
|
63
|
-
| `Set<T>`
|
|
64
|
-
| `new Set(array)`
|
|
65
|
-
|
|
66
|
-
| object
|
|
67
|
-
| Nested class
|
|
68
|
-
| Calls `fromJSON()` if available
|
|
69
|
-
|
|
70
|
-
## Serde Options
|
|
71
|
-
|
|
72
|
-
Use the `@serde` decorator to customize deserialization:
|
|
73
|
-
|
|
74
|
-
### Renaming Fields
|
|
75
|
-
|
|
76
|
-
<MacroExample before={data.examples.rename.before} after={data.examples.rename.after} />
|
|
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';
|
|
77
246
|
|
|
78
|
-
|
|
247
|
+
class User {
|
|
248
|
+
id: string;
|
|
249
|
+
|
|
250
|
+
name: string;
|
|
251
|
+
|
|
252
|
+
constructor(props: {
|
|
253
|
+
id: string;
|
|
254
|
+
name: string;
|
|
255
|
+
}) {
|
|
256
|
+
this.id = props.id;
|
|
257
|
+
this.name = props.name;
|
|
258
|
+
}
|
|
259
|
+
|
|
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
|
+
}
|
|
286
|
+
|
|
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
|
+
}
|
|
326
|
+
|
|
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
|
+
}
|
|
377
|
+
|
|
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
|
+
}
|
|
387
|
+
|
|
388
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
389
|
+
field: string;
|
|
390
|
+
message: string;
|
|
391
|
+
}> {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
``` ```
|
|
79
396
|
const user = User.fromJSON({ user_id: "123", full_name: "Alice" });
|
|
80
397
|
console.log(user.id); // "123"
|
|
81
398
|
console.log(user.name); // "Alice"
|
|
399
|
+
``` ### Default Values
|
|
400
|
+
**Before:**
|
|
82
401
|
```
|
|
402
|
+
/** @derive(Deserialize) */
|
|
403
|
+
class Config {
|
|
404
|
+
host: string;
|
|
83
405
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<MacroExample before={data.examples.default.before} after={data.examples.default.after} />
|
|
406
|
+
/** @serde({ default: "3000" }) */
|
|
407
|
+
port: string;
|
|
87
408
|
|
|
88
|
-
|
|
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';
|
|
420
|
+
|
|
421
|
+
class Config {
|
|
422
|
+
host: string;
|
|
423
|
+
|
|
424
|
+
port: string;
|
|
425
|
+
|
|
426
|
+
debug: boolean;
|
|
427
|
+
|
|
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
|
+
}
|
|
437
|
+
|
|
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
|
+
}
|
|
464
|
+
|
|
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
|
+
}
|
|
504
|
+
|
|
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
|
+
}
|
|
557
|
+
|
|
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
|
+
}
|
|
567
|
+
|
|
568
|
+
static validateFields(partial: Partial<Config>): Array<{
|
|
569
|
+
field: string;
|
|
570
|
+
message: string;
|
|
571
|
+
}> {
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
``` ```
|
|
89
576
|
const config = Config.fromJSON({ host: "localhost" });
|
|
90
577
|
console.log(config.port); // "3000"
|
|
91
578
|
console.log(config.debug); // false
|
|
579
|
+
``` ### Skipping Fields
|
|
580
|
+
**Source:**
|
|
92
581
|
```
|
|
93
|
-
|
|
94
|
-
### Skipping Fields
|
|
95
|
-
|
|
96
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
582
|
+
/** @derive(Deserialize) */
|
|
97
583
|
class User {
|
|
98
584
|
name: string;
|
|
99
585
|
email: string;
|
|
@@ -103,31 +589,25 @@ class User {
|
|
|
103
589
|
|
|
104
590
|
/** @serde({ skip_deserializing: true }) */
|
|
105
591
|
computedField: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
### Deny Unknown Fields
|
|
114
|
-
|
|
115
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
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) */
|
|
116
598
|
/** @serde({ deny_unknown_fields: true }) */
|
|
117
599
|
class StrictUser {
|
|
118
600
|
name: string;
|
|
119
601
|
email: string;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
602
|
+
}
|
|
603
|
+
``` ```
|
|
123
604
|
// This will throw an error
|
|
124
605
|
StrictUser.fromJSON({ name: "Alice", email: "a@b.com", extra: "field" });
|
|
125
606
|
// Error: StrictUser.fromJSON: unknown field "extra"
|
|
607
|
+
``` ### Flatten Nested Objects
|
|
608
|
+
**Source:**
|
|
126
609
|
```
|
|
127
|
-
|
|
128
|
-
### Flatten Nested Objects
|
|
129
|
-
|
|
130
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
610
|
+
/** @derive(Deserialize) */
|
|
131
611
|
class Address {
|
|
132
612
|
city: string;
|
|
133
613
|
zip: string;
|
|
@@ -139,9 +619,8 @@ class User {
|
|
|
139
619
|
|
|
140
620
|
/** @serde({ flatten: true }) */
|
|
141
621
|
address: Address;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
622
|
+
}
|
|
623
|
+
``` ```
|
|
145
624
|
// Flat JSON structure
|
|
146
625
|
const user = User.fromJSON({
|
|
147
626
|
name: "Alice",
|
|
@@ -149,49 +628,153 @@ const user = User.fromJSON({
|
|
|
149
628
|
zip: "10001"
|
|
150
629
|
});
|
|
151
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:**
|
|
152
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
|
+
}
|
|
153
669
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
``` ```
|
|
195
778
|
const json = { status: 200, message: "OK", timestamp: "2024-01-15T10:30:00.000Z" };
|
|
196
779
|
|
|
197
780
|
// Type guard
|
|
@@ -202,15 +785,43 @@ if (ApiResponse.is(json)) {
|
|
|
202
785
|
// Deserialize with validation
|
|
203
786
|
const response = ApiResponse.fromJSON(json);
|
|
204
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:**
|
|
205
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';
|
|
206
802
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
803
|
+
enum Status {
|
|
804
|
+
Active = 'active',
|
|
805
|
+
Inactive = 'inactive',
|
|
806
|
+
Pending = 'pending'
|
|
807
|
+
}
|
|
212
808
|
|
|
213
|
-
|
|
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
|
+
``` ```
|
|
214
825
|
const status = Status.fromJSON("active");
|
|
215
826
|
console.log(status); // Status.Active
|
|
216
827
|
|
|
@@ -220,29 +831,151 @@ try {
|
|
|
220
831
|
} catch (e) {
|
|
221
832
|
console.log(e.message); // "Invalid Status value: invalid"
|
|
222
833
|
}
|
|
834
|
+
``` Works with numeric enums too:
|
|
835
|
+
**Source:**
|
|
223
836
|
```
|
|
224
|
-
|
|
225
|
-
Works with numeric enums too:
|
|
226
|
-
|
|
227
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
837
|
+
/** @derive(Deserialize) */
|
|
228
838
|
enum Priority {
|
|
229
839
|
Low = 1,
|
|
230
840
|
Medium = 2,
|
|
231
841
|
High = 3,
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
842
|
+
}
|
|
843
|
+
``` ```
|
|
235
844
|
const priority = Priority.fromJSON(3);
|
|
236
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:**
|
|
237
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';
|
|
864
|
+
|
|
865
|
+
type UserProfile = {
|
|
866
|
+
id: string;
|
|
867
|
+
name: string;
|
|
868
|
+
createdAt: Date;
|
|
869
|
+
};
|
|
238
870
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
977
|
+
}
|
|
978
|
+
``` ```
|
|
246
979
|
const json = {
|
|
247
980
|
id: "123",
|
|
248
981
|
name: "Alice",
|
|
@@ -251,31 +984,26 @@ const json = {
|
|
|
251
984
|
|
|
252
985
|
const profile = UserProfile.fromJSON(json);
|
|
253
986
|
console.log(profile.createdAt instanceof Date); // true
|
|
987
|
+
``` For union types, basic validation is applied:
|
|
988
|
+
**Source:**
|
|
254
989
|
```
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
259
|
-
type ApiStatus = "loading" | "success" | "error";`} />
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
990
|
+
/** @derive(Deserialize) */
|
|
991
|
+
type ApiStatus = "loading" | "success" | "error";
|
|
992
|
+
``` ```
|
|
262
993
|
const status = ApiStatus.fromJSON("success");
|
|
263
994
|
console.log(status); // "success"
|
|
995
|
+
``` ## Combining with Serialize
|
|
996
|
+
Use both Serialize and Deserialize for complete JSON round-trip support:
|
|
997
|
+
**Source:**
|
|
264
998
|
```
|
|
265
|
-
|
|
266
|
-
## Combining with Serialize
|
|
267
|
-
|
|
268
|
-
Use both Serialize and Deserialize for complete JSON round-trip support:
|
|
269
|
-
|
|
270
|
-
<InteractiveMacro code={`/** @derive(Serialize, Deserialize) */
|
|
999
|
+
/** @derive(Serialize, Deserialize) */
|
|
271
1000
|
/** @serde({ rename_all: "camelCase" }) */
|
|
272
1001
|
class UserProfile {
|
|
273
1002
|
user_name: string;
|
|
274
1003
|
created_at: Date;
|
|
275
1004
|
is_active: boolean;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
1005
|
+
}
|
|
1006
|
+
``` ```
|
|
279
1007
|
// Create and serialize
|
|
280
1008
|
const profile = new UserProfile();
|
|
281
1009
|
profile.user_name = "Alice";
|
|
@@ -289,19 +1017,16 @@ const json = JSON.stringify(profile);
|
|
|
289
1017
|
const restored = UserProfile.fromJSON(JSON.parse(json));
|
|
290
1018
|
console.log(restored.user_name); // "Alice"
|
|
291
1019
|
console.log(restored.created_at instanceof Date); // true
|
|
1020
|
+
``` ## Error Handling
|
|
1021
|
+
Handle deserialization errors gracefully:
|
|
1022
|
+
**Source:**
|
|
292
1023
|
```
|
|
293
|
-
|
|
294
|
-
## Error Handling
|
|
295
|
-
|
|
296
|
-
Handle deserialization errors gracefully:
|
|
297
|
-
|
|
298
|
-
<InteractiveMacro code={`/** @derive(Deserialize) */
|
|
1024
|
+
/** @derive(Deserialize) */
|
|
299
1025
|
class User {
|
|
300
1026
|
name: string;
|
|
301
1027
|
email: string;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
1028
|
+
}
|
|
1029
|
+
``` ```
|
|
305
1030
|
function parseUser(json: unknown): User | null {
|
|
306
1031
|
try {
|
|
307
1032
|
return User.fromJSON(json);
|