# 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`