Skip to content

Byte Layout

The format is tagless and positional. Payloads contain no field names, type markers, separators, or version bytes; the schema supplies their meaning. Every example below comes from the published implementation.

Unsigned: base-128 varints, little-endian groups, high bit as continuation flag.

ValueBytes
0[0]
127[127]
128[128, 1]

Signed: ZigZag first, mapping 0, -1, 1, -2, 2 to 0, 1, 2, 3, 4, then the same varint.

ValueBytes
-1[1]
63[126]
64[128, 1]

ZigZag doubles the magnitude, so a signed integer crosses every size boundary at half the value: 64 is two bytes as an int, one as a uint. Declare minimum >= 0 wherever it is true.

Overlong varints are rejected. 1 must be [1], never [129, 0].

z.number() is little-endian IEEE-754 float64, always 8 bytes, no varint compaction.

1.5 -> [0, 0, 0, 0, 0, 0, 248, 63]

m.float32() (4 bytes) is available only through the low-level API. Little-endian order is part of the format and does not depend on the host. An optimization using Float64Array was rejected because it would reverse the bytes on a big-endian host.

One byte, [1] or [0]. Anything else is a DecodeError.

A varint byte length, then the contents. Strings are UTF-8; m.bytes() is raw.

"ab" -> [2, 97, 98]
Uint8Array([9, 9]) -> [2, 9, 9]

UTF-8 decoding is strict. Invalid sequences cause a DecodeError instead of being replaced with U+FFFD.

Zero bytes — the schema already knows the value.

m.literal("x") with "x" -> []

The index of the value in sorted order, as a varint. Members are deduplicated and sorted first, so declaration order is irrelevant.

m.enum(["M", "F", "X"]) // sorted to ["F", "M", "X"]
"X" -> [2]

An index past the last member is a DecodeError. Adding a member shifts every index at or after it — see Fingerprinting.

One discriminator byte, then the value if present.

null -> [0]
5 -> [1, 5]

A varint element count, then elements back to back. Order is never changed.

[1, 2, 3] -> [3, 1, 2, 3]

The decoder refuses a count larger than the remaining input could satisfy, before allocating — see Hostile Input.

Elements only; the length comes from the schema.

m.tuple([m.uint(), m.boolean()]) with [7, true] -> [7, 1]

Because the length is not on the wire, a tuple may contain zero-width elements where an array may not.

  1. A presence bitmap for the optional fields, ceil(n / 8) bytes. Omitted entirely when there are none.
  2. The field values in canonical key order, skipping absent optionals.

A field’s bit is its rank among the optional fields, low bit first.

m.object({ a: m.uint().optional(), b: m.uint() })
{ a: 1, b: 2 } -> [1, 1, 2] // bitmap 1, then a, then b
{ b: 2 } -> [0, 2] // bitmap 0, a skipped

Nine optional fields make the bitmap two bytes:

9 optional, all absent -> [0, 0]
9 optional, all present, each 1 -> [255, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Field order is the field name’s rank in ascending UTF-16 code-unit order, applied by the encoder, never declared.

The bitmap width is fixed by the schema — which is why append-only compatibility fails: a ninth optional field adds a byte and every earlier payload misreads. See Schema Evolution.

encode(Person, { name: "Grace", age: 45, sex: "F" });
[45, 5, 71, 114, 97, 99, 101, 0]
│ │ └────────────────────┘ └─ sex: index of "F" in ["F","M","X"]
│ └─ name: length 5
└─ age: uint varint 45 (no bitmap: nothing is optional)

Eight bytes. age comes first because "age" sorts before "name". JSON spends 35.

LimitValue
Collection elements1,000,000
String / byte-array length64 MiB
Trailing bytesrejected
Non-canonical varintrejected
Unsafe numeric varintrejected

No schema identifier, version byte, length prefix on the whole value, or type tags. The format version is hashed into the fingerprint instead of spent as a wire byte.

Bare payloads are compact, but they are neither self-describing nor confidential. Encrypt them when secrecy is required.