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 encoders handle cyclic graphs by assigning identities and emitting back-references.
/** @derive(Encode, Decode) */
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.encode(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 decode a cyclic graph back, since the ids are what reconnect it.
Node.encode(root); // clean payload, cycles flattened
Node.encode(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
encodeWithContext / decodeWithContext take a context that carries the identity map. The top-level encode / decode create one for you; you only need the context form when composing encoders by hand.
Forward references
While decoding, a reference may point at an object that hasn’t been constructed yet. Rather than failing, the decoder 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-decoding you may observe that. ctx.applyPatches()performs the fix-up. The top-leveldecodecalls it for you.
freeze
Pass freeze: true to deep-freeze every object produced:
const r = Node.decode(input, { freeze: true });Freezing happens after patches are applied, so cyclic graphs freeze correctly rather than being frozen mid-construction.
Errors
Structural failures inside decodeWithContext throw DecodeError. The top-level decode catches these and returns { success: false, errors } instead: so the entry point never throws, while the internal context form does.