Tramaj — Normative Node JSON representation
On Fri, 02 Oct 2026, by @lucasdicioccio, 525 words, 5 code snippets, 0 links, 0images.
Generated from specs/node-json.md — the repository is the canonical source, and may be ahead of this page.
Tramaj — Normative Node JSON representation
The evaluated Node AST is the portable interchange boundary between Tramaj
implementations and their hosts. This document defines its normative JSON
representation.
An implementation MAY use any internal representation of nodes, but a conforming
implementation MUST be able to produce and consume the representation below
without loss: decode(encode(n)) MUST yield a node equal to n.
Object key ordering is not semantically significant and MUST NOT be relied upon.
Node AST
Annotations = Map<String, JSON>
Node
= Text
value: Value
annotations: Annotations
| Element
tag: String
attributes: List<NodeAttribute>
value: Value
children: List<Node>
annotations: Annotations
| Fragment
children: List<Node>
annotations: Annotations
NodeAttribute
= Attribute
name: String
value: Value
| Action
event: String
key: String
payload: Value
Value is the ordinary JSON value domain — null, boolean, number, string,
array, object. It is not a Node: a document tree never nests through an
attribute value, an element value slot, or an action payload.
Text carries a Value, not a String, so scalars survive evaluation without
implicit conversion: .p($ctx.count) with count = 3 produces a text node whose
value is the number 3, not the string "3". A host that wants a string renders
one; the interchange format does not decide that for it.
Element carries a value slot alongside its children, for hosts whose target
format attaches a body value to a tagged node (a scalar leaf in YAML/HCL, a
configuration value, an editor model). It defaults to null when a template does
not set one.
Fragment is an ordered sequence of siblings introducing no wrapper element. It
is a real node in this representation; a host MAY flatten fragments when folding
into its own target.
JSON encoding
Every object carries a discriminator: "type" for nodes, "kind" for attributes.
Every field listed is REQUIRED — including "annotations", which is {} when a
node has none, and "value", which is null when an element has no value slot.
An encoder MUST emit all of them; a decoder MUST reject an object missing any.
Text
{"type": "text", "value": 3, "annotations": {}}{"type": "text", "value": "hello", "annotations": {}}Element
{
"type": "element",
"tag": "button",
"attributes": [
{"kind": "attribute", "name": "class", "value": "primary"},
{"kind": "action", "event": "on-click", "key": "deploy",
"payload": {"deployment": "web"}}
],
"value": null,
"children": [
{"type": "text", "value": "Deploy", "annotations": {}}
],
"annotations": {}
}attributes is an ordered list, not a map: an element may carry any number of
attributes and any number of actions, and their source order is preserved.
Ordinary attributes and actions share one list because both are attribute-position
constructs; a host reading only one kind filters on "kind".
An attribute name may repeat. The representation does not deduplicate; a host
decides what a repeated name means for its target.
Fragment
{
"type": "fragment",
"children": [
{"type": "text", "value": "one", "annotations": {}},
{"type": "text", "value": "two", "annotations": {}}
],
"annotations": {}
}Annotations
annotations is a JSON object mapping annotation keys to arbitrary JSON.
The core language assigns no meaning to any key or value. Annotations are the extension point for types, domains and constraints, provenance, host metadata, and optimization hints.
Unknown annotations MUST NOT affect core semantics, and implementations MUST
preserve them across every node-to-node transformation they perform —
adapt-actions in particular copies a node’s annotations through unchanged.
Decoding
A decoder MUST reject:
- an object with no
"type", or a"type"outside{text, element, fragment}; - an attribute with no
"kind", or a"kind"outside{attribute, action}; - any object missing a field required for its discriminator;
- a non-string
tag,name,event, orkey; - a non-array
attributesorchildren, or a non-objectannotations.
A decoder MUST NOT infer a missing field from a default. Defaults are an encoder’s
job; on the wire the representation is explicit, so that a missing field is a bug
rather than a silent null.