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

TypeGenerated CodeDescription
ClassclassNameSerialize(value) + static serialize(value)Standalone function + static wrapper method
EnumenumNameSerialize(value), enumNameSerializeWithContextStandalone functions
InterfaceinterfaceNameSerialize(value), etc.Standalone functions
Type AliastypeNameSerialize(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:

  1. Check if it’s already been serialized (has an __id)
  2. If so, return { "__ref": existingId } instead
  3. Otherwise, register the object and serialize its fields

Type-Specific Serialization

TypeSerialization Strategy
PrimitivesDirect value
DatetoISOString()
ArraysFor 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>
NullableInclude null explicitly; for primitive-like and Date unions the generator avoids runtime SerializeWithContext checks
ObjectsCall 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 serialization
  • rename = "jsonKey" - Use different JSON property name
  • flatten - 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.