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, keepMetadata?) + static serialize(value, keepMetadata?) | 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> |
Record<K,V> | For primitive-like values, pass through; for Date/`Date |
Wrappers (Partial<T>, Pick<T,K>, …) | Serialize based on the inner type T |
| Nullable | Include null explicitly; non-null values follow the inner type’s strategy |
| Objects | Call the type’s nameSerializeWithContext function |
Note: which strategy applies is resolved statically from the field’s declared TypeScript type at expansion time — there is no runtime feature detection.
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, keepMetadata?: boolean): string {
return userSerialize(value, keepMetadata);
}
/** @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: __mf_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,
keepMetadata?: boolean
): string {
const ctx = __mf_SerializeContext.create();
const __raw = userSerializeWithContext(value, ctx);
if (keepMetadata) return JSON.stringify(__raw);
return JSON.stringify(__raw, (key, val) => key === "__type" || key === "__id" ? undefined : val);
} /** @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; // tag field name is configurable via `tag` option
Object.assign(result, rest);
}
return result;
}Required Import
The generated code automatically imports SerializeContext from macroforge/serde.