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.
Integers
Section titled “Integers”Unsigned: base-128 varints, little-endian groups, high bit as continuation flag.
| Value | Bytes |
|---|---|
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.
| Value | Bytes |
|---|---|
-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].
Floats
Section titled “Floats”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.
Booleans
Section titled “Booleans”One byte, [1] or [0]. Anything else is a DecodeError.
Strings and bytes
Section titled “Strings and bytes”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.
Literals
Section titled “Literals”Zero bytes — the schema already knows the value.
m.literal("x") with "x" -> []String enums
Section titled “String enums”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.
Nullable
Section titled “Nullable”One discriminator byte, then the value if present.
null -> [0]5 -> [1, 5]Arrays
Section titled “Arrays”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.
Tuples
Section titled “Tuples”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.
Objects
Section titled “Objects”- A presence bitmap for the optional fields,
ceil(n / 8)bytes. Omitted entirely when there are none. - 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 skippedNine 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.
A whole record
Section titled “A whole record”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.
Decoder limits
Section titled “Decoder limits”| Limit | Value |
|---|---|
| Collection elements | 1,000,000 |
| String / byte-array length | 64 MiB |
| Trailing bytes | rejected |
| Non-canonical varint | rejected |
| Unsafe numeric varint | rejected |
What is not in the payload
Section titled “What is not in the payload”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.