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.
Cycles & Forward References
JSON.stringify throws on a cycle. Macroforge’s generated serializers handle cyclic graphs by assigning identities and emitting back-references.
/** @derive(Serialize, Deserialize) */
class Node {
name: string;
parent?: Node;
children: Node[];
}
const root = new Node();
const child = new Node();
root.children = [child];
child.parent = root; // cycle
const json = Node.serialize(root, true); // keepMetadataMetadata
Cycle detection works by tagging objects with __type and __id as they’re visited; a repeat visit emits a reference to the existing id instead of recursing.
Those fields are stripped from the output by default. Pass keepMetadata: true to retain them — which you must do if you intend to deserialize a cyclic graph back, since the ids are what reconnect it.
Node.serialize(root); // clean payload, cycles flattened
Node.serialize(root, true); // round-trippable, includes __type/__idFor API responses you usually want the default. For persistence or transferring an object graph, keep the metadata.
The context object
serializeWithContext / deserializeWithContext take a context that carries the identity map. The top-level serialize / deserialize create one for you; you only need the context form when composing serializers by hand.
Forward references
While deserializing, a reference may point at an object that hasn’t been constructed yet. Rather than failing, the deserializer records a PendingRef placeholder and patches it once the target exists.
Two consequences worth knowing:
- The field is temporarily
nullbetween the placeholder being recorded and patches being applied. If you read the graph mid-deserialization you may observe that. ctx.applyPatches()performs the fix-up. The top-leveldeserializecalls it for you.
freeze
Pass freeze: true to deep-freeze every object produced:
const r = Node.deserialize(input, { freeze: true });Freezing happens after patches are applied, so cyclic graphs freeze correctly rather than being frozen mid-construction.
Errors
Structural failures inside deserializeWithContext throw DeserializeError. The top-level deserialize catches these and returns { success: false, errors } instead — so the entry point never throws, while the internal context form does.