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.

Encode

The Encode macro generates JSON encoding methods with cycle detection and object identity tracking. This enables encoding of complex object graphs including circular references.

Generated Methods

TypeGenerated CodeDescription
ClassclassNameEncode(value, keepMetadata?) + static encode(value, keepMetadata?)Standalone function + static wrapper method
EnumenumNameEncode(value), enumNameEncodeWithContextStandalone functions
InterfaceinterfaceNameEncode(value), etc.Standalone functions
Type AliastypeNameEncode(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 encoded:

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

Type-Specific Encoding

TypeEncoding 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>
Record<K,V>For primitive-like values, pass through; for Date/`Date
Wrappers (Partial<T>, Pick<T,K>, …)Encode based on the inner type T
NullableInclude null explicitly; non-null values follow the inner type’s strategy
ObjectsCall the type’s nameEncodeWithContext 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 @endec decorator supports:

  • skip / skipEncoding - Exclude field from encoding
  • rename = "jsonKey" - Use different JSON property name
  • flatten - Merge nested object’s fields into parent

Example

Before (Your Code)
/** @derive(Encode) */
class User {
    id: number;

    /** @endec({ rename: "userName" }) */
    name: string;

    /** @endec({ skipEncoding: true }) */
    password: string;

    /** @endec({ flatten: true }) */
    metadata: UserMetadata;
}
After (Generated)
import { EncodeContext } from '@macroforge/core/endec';

class User {
    id: number;

    name: string;

    password: string;

    metadata: UserMetadata;
    /** Encodes a value to a JSON string.
@param value - The value to encode
@returns JSON string representation with cycle detection metadata  */

    static encode(value: User, keepMetadata?: boolean): string {
        return userEncode(value, keepMetadata);
    }
    /** @internal Encodes with an existing context for nested/cyclic object graphs.
@param value - The value to encode
@param ctx - The encoding context  */

    static encodeWithContext(value: User, ctx: __mf_EncodeContext): Record<string, unknown> {
        return userEncodeWithContext(value, ctx);
    }
}

/** Encodes a value to a JSON string.
@param value - The value to encode
@returns JSON string representation with cycle detection metadata */ export function userEncode(
    value: User,
    keepMetadata?: boolean
): string {
    const ctx = __mf_EncodeContext.create();
    const __raw = userEncodeWithContext(value, ctx);
    if (keepMetadata) return JSON.stringify(__raw);
    return JSON.stringify(__raw, (key, val) => key === "__type" || key === "__id" ? undefined : val);
} /** @internal Encodes with an existing context for nested/cyclic object graphs.
@param value - The value to encode
@param ctx - The encoding context */
export function userEncodeWithContext(
    value: User,
    ctx: EncodeContext
): 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 = userMetadataEncodeWithContext(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 EncodeContext from @macroforge/core/endec.