Skip to main content
Glama
deserialize.md10.1 kB
# Deserialize 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. ## Generated Output | Type | Generated Code | Description | |------|----------------|-------------| | Class | `classNameDeserialize(input)` + `static deserialize(input)` | Standalone function + static factory method | | Enum | `enumNameDeserialize(input)`, `enumNameDeserializeWithContext(data)`, `enumNameIs(value)` | Standalone functions | | Interface | `interfaceNameDeserialize(input)`, etc. | Standalone functions | | Type Alias | `typeNameDeserialize(input)`, etc. | Standalone functions | ## Return Type All public deserialization methods return `Result<T, Array<{ field: string; message: string }>>`: - `Result.ok(value)` - Successfully deserialized value - `Result.err(errors)` - Array of validation errors with field names and messages ## Cycle/Forward-Reference Support Uses deferred patching to handle references: 1. When encountering `{ "__ref": id }`, returns a `PendingRef` marker 2. Continues deserializing other fields 3. After all objects are created, `ctx.applyPatches()` resolves all pending references References only apply to object-shaped, serializable values. The generator avoids probing for `__ref` on primitive-like fields (including literal unions and `T | null` where `T` is primitive-like), and it parses `Date` / `Date | null` from ISO strings without treating them as references. ## Validation The macro supports 30+ validators via `@serde(validate(...))`: ### String Validators - `email`, `url`, `uuid` - Format validation - `minLength(n)`, `maxLength(n)`, `length(n)` - Length constraints - `pattern("regex")` - Regular expression matching - `nonEmpty`, `trimmed`, `lowercase`, `uppercase` - String properties ### Number Validators - `gt(n)`, `gte(n)`, `lt(n)`, `lte(n)`, `between(min, max)` - Range checks - `int`, `positive`, `nonNegative`, `finite` - Number properties ### Array Validators - `minItems(n)`, `maxItems(n)`, `itemsCount(n)` - Collection size ### Date Validators - `validDate`, `afterDate("ISO")`, `beforeDate("ISO")` - Date validation ## Field-Level Options The `@serde` decorator supports: - `skip` / `skipDeserializing` - Exclude field from deserialization - `rename = "jsonKey"` - Read from different JSON property - `default` / `default = expr` - Use default value if missing - `flatten` - Read fields from parent object level - `validate(...)` - Apply validators ## Container-Level Options - `denyUnknownFields` - Error on unrecognized JSON properties - `renameAll = "camelCase"` - Apply naming convention to all fields ## Union Type Deserialization Union types are deserialized based on their member types: ### Literal Unions For unions of literal values (`"A" | "B" | 123`), the value is validated against the allowed literals directly. ### Primitive Unions For unions containing primitive types (`string | number`), the deserializer uses `typeof` checks to validate the value type. No `__type` discriminator is needed. ### Class/Interface Unions For unions of serializable types (`User | Admin`), the deserializer requires a `__type` field in the JSON to dispatch to the correct type's `deserializeWithContext` method. ### Generic Type Parameters For generic unions like `type Result<T> = T | Error`, the generic type parameter `T` is passed through as-is since its concrete type is only known at the call site. ### Mixed Unions Mixed unions (e.g., `string | Date | User`) check in order: 1. Literal values 2. Primitives (via `typeof`) 3. Date (via `instanceof` or ISO string parsing) 4. Serializable types (via `__type` dispatch) 5. Generic type parameters (pass-through) ## Example ```typescript before /** @derive(Deserialize) @serde({ denyUnknownFields: true }) */ class User { id: number; /** @serde({ validate: { email: true, maxLength: 255 } }) */ email: string; /** @serde({ default: "guest" }) */ name: string; /** @serde({ validate: { positive: true } }) */ age?: number; } ``` ```typescript after import { DeserializeContext } from 'macroforge/serde'; import { DeserializeError } from 'macroforge/serde'; import type { DeserializeOptions } from 'macroforge/serde'; import { PendingRef } from 'macroforge/serde'; /** @serde({ denyUnknownFields: true }) */ class User { id: number; email: string; name: string; age?: number; constructor(props: { id: number; email: string; name?: string; age?: number; }) { this.id = props.id; this.email = props.email; this.name = props.name as string; this.age = props.age as number; } /** * Deserializes input to an instance of this class. * Automatically detects whether input is a JSON string or object. * @param input - JSON string or object to deserialize * @param opts - Optional deserialization options * @returns Result containing the deserialized instance or validation errors */ static deserialize( input: unknown, opts?: @{DESERIALIZE_OPTIONS} ): Result< User, Array<{ field: string; message: string; }> > { try { // Auto-detect: if string, parse as JSON first const data = typeof input === 'string' ? JSON.parse(input) : input; const ctx = @{DESERIALIZE_CONTEXT}.create(); const resultOrRef = User.deserializeWithContext(data, ctx); if (@{PENDING_REF}.is(resultOrRef)) { return Result.err([ { field: '_root', message: 'User.deserialize: root cannot be a forward reference' } ]); } ctx.applyPatches(); if (opts?.freeze) { ctx.freezeAll(); } return Result.ok(resultOrRef); } catch (e) { if (e instanceof @{DESERIALIZE_ERROR}) { return Result.err(e.errors); } const message = e instanceof Error ? e.message : String(e); return Result.err([ { field: '_root', message } ]); } } /** @internal */ static deserializeWithContext(value: any, ctx: @{DESERIALIZE_CONTEXT}): User | @{PENDING_REF} { if (value?.__ref !== undefined) { return ctx.getOrDefer(value.__ref); } if (typeof value !== 'object' || value === null || Array.isArray(value)) { throw new @{DESERIALIZE_ERROR}([ { field: '_root', message: 'User.deserializeWithContext: expected an object' } ]); } const obj = value as Record<string, unknown>; const errors: Array<{ field: string; message: string; }> = []; const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']); for (const key of Object.keys(obj)) { if (!knownKeys.has(key)) { errors.push({ field: key, message: 'unknown field' }); } } if (!('id' in obj)) { errors.push({ field: 'id', message: 'missing required field' }); } if (!('email' in obj)) { errors.push({ field: 'email', message: 'missing required field' }); } if (errors.length > 0) { throw new @{DESERIALIZE_ERROR}(errors); } const instance = Object.create(User.prototype) as User; if (obj.__id !== undefined) { ctx.register(obj.__id as number, instance); } ctx.trackForFreeze(instance); { const __raw_id = obj['id'] as number; instance.id = __raw_id; } { const __raw_email = obj['email'] as string; instance.email = __raw_email; } if ('name' in obj && obj['name'] !== undefined) { const __raw_name = obj['name'] as string; instance.name = __raw_name; } else { instance.name = "guest"; } if ('age' in obj && obj['age'] !== undefined) { const __raw_age = obj['age'] as number; instance.age = __raw_age; } if (errors.length > 0) { throw new @{DESERIALIZE_ERROR}(errors); } return instance; } static validateField<K extends keyof User>( field: K, value: User[K] ): Array<{ field: string; message: string; }> { return []; } static validateFields(partial: Partial<User>): Array<{ field: string; message: string; }> { return []; } static hasShape(obj: unknown): boolean { if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { return false; } const o = obj as Record<string, unknown>; return 'id' in o && 'email' in o; } static is(obj: unknown): obj is User { if (obj instanceof User) { return true; } if (!User.hasShape(obj)) { return false; } const result = User.deserialize(obj); return Result.isOk(result); } } // Usage: const result = User.deserialize('{"id":1,"email":"test@example.com"}'); if (Result.isOk(result)) { const user = result.value; } else { console.error(result.error); // [{ field: "email", message: "must be a valid email" }] } ``` ## Required Imports The generated code automatically imports: - `DeserializeContext`, `DeserializeError`, `PendingRef` from `macroforge/serde`

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/macroforge-ts/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server