API Stability Notice
Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.
Serialize
The Serialize macro generates JSON serialization methods with cycle detection and object identity tracking. This enables serialization of complex object graphs
including circular references.
Generated Methods
| Type | Generated Code | Description |
|---|---|---|
| Class | classNameSerialize(value) + static serialize(value) | Standalone function + static wrapper method |
| Enum | enumNameSerialize(value), enumNameSerializeWithContext | Standalone functions |
| Interface | interfaceNameSerialize(value), etc. | Standalone functions |
| Type Alias | typeNameSerialize(value), etc. | Standalone functions |
Cycle Detection Protocol
The generated code handles circular references using __id and __ref markers:
{
"__type": "User",
"__id": 1,
"name": "Alice",
"friend": { "__ref": 2 } // Reference to object with __id: 2
}When an object is serialized:
- Check if it’s already been serialized (has an
__id) - If so, return
{ "__ref": existingId }instead - Otherwise, register the object and serialize its fields
Type-Specific Serialization
| Type | Serialization Strategy |
|---|---|
| Primitives | Direct value |
Date | toISOString() |
| Arrays | For primitive-like element types, pass through; for Date/`Date |
Map<K,V> | For primitive-like values, Object.fromEntries(map.entries()); for Date/`Date |
Set<T> | Convert to array; element handling matches Array<T> |
| Nullable | Include null explicitly; for primitive-like and Date unions the generator avoids runtime SerializeWithContext checks |
| Objects | Call SerializeWithContext(ctx) if available (to support user-defined implementations) |
Note: the generator specializes some code paths based on the declared TypeScript type to avoid runtime feature detection on primitives and literal unions.
Field-Level Options
The @serde decorator supports:
skip/skipSerializing- Exclude field from serializationrename = "jsonKey"- Use different JSON property nameflatten- Merge nested object’s fields into parent
Example
Before (Your Code)
/** @derive(Serialize) */
class User {
id: number;
/** @serde({ rename: "userName" }) */
name: string;
/** @serde({ skipSerializing: true }) */
password: string;
/** @serde({ flatten: true }) */
metadata: UserMetadata;
} After (Generated)
import { SerializeContext } from 'macroforge/serde';
class User {
id: number;
name: string;
password: string;
metadata: UserMetadata;
/** Serializes a value to a JSON string.
@param value - The value to serialize
@returns JSON string representation with cycle detection metadata */
static serialize(value: User): string {
return userSerialize(value);
}
/** @internal Serializes with an existing context for nested/cyclic object graphs.
@param value - The value to serialize
@param ctx - The serialization context */
static serializeWithContext(value: User, ctx: SerializeContext): Record<string, unknown> {
return userSerializeWithContext(value, ctx);
}
}
/** Serializes a value to a JSON string.
@param value - The value to serialize
@returns JSON string representation with cycle detection metadata */ export function userSerialize(
value: User
): string {
const ctx = SerializeContext.create();
return JSON.stringify(userSerializeWithContext(value, ctx));
} /** @internal Serializes with an existing context for nested/cyclic object graphs.
@param value - The value to serialize
@param ctx - The serialization context */
export function userSerializeWithContext(
value: User,
ctx: SerializeContext
): Record<string, unknown> {
const existingId = ctx.getId(value);
if (existingId !== undefined) {
return { __ref: existingId };
}
const __id = ctx.register(value);
const result: Record<string, unknown> = { __type: 'User', __id };
result['id'] = value.id;
result['userName'] = value.name;
{
const __flattened = userMetadataSerializeWithContext(value.metadata, ctx);
const { __type: _, __id: __, ...rest } = __flattened as any;
Object.assign(result, rest);
}
return result;
}Generated output:
import { SerializeContext } from 'macroforge/serde';
class User {
id: number;
name: string;
password: string;
metadata: UserMetadata;
/** Serializes a value to a JSON string.
@param value - The value to serialize
@returns JSON string representation with cycle detection metadata */
static serialize(value: User): string {
return userSerialize(value);
}
/** @internal Serializes with an existing context for nested/cyclic object graphs.
@param value - The value to serialize
@param ctx - The serialization context */
static serializeWithContext(value: User, ctx: SerializeContext): Record<string, unknown> {
return userSerializeWithContext(value, ctx);
}
}
/** Serializes a value to a JSON string.
@param value - The value to serialize
@returns JSON string representation with cycle detection metadata */ export function userSerialize(
value: User
): string {
const ctx = SerializeContext.create();
return JSON.stringify(userSerializeWithContext(value, ctx));
} /** @internal Serializes with an existing context for nested/cyclic object graphs.
@param value - The value to serialize
@param ctx - The serialization context */
export function userSerializeWithContext(
value: User,
ctx: SerializeContext
): Record<string, unknown> {
const existingId = ctx.getId(value);
if (existingId !== undefined) {
return { __ref: existingId };
}
const __id = ctx.register(value);
const result: Record<string, unknown> = { __type: 'User', __id };
result['id'] = value.id;
result['userName'] = value.name;
{
const __flattened = userMetadataSerializeWithContext(value.metadata, ctx);
const { __type: _, __id: __, ...rest } = __flattened as any;
Object.assign(result, rest);
}
return result;
}Required Import
The generated code automatically imports SerializeContext from macroforge/serde.