Type Encoding #
Every value is encoded recursively. The integer encoding is a fundamental building block — it's used for length prefixes, indices, and option selectors.
Each type is defined by its schema declaration. Bounds from the schema determine how the value is encoded and validated.
Null #
Null is encoded as a null integer value (see [Integer Encoding: Null](integer.md#null)).
A null value can only appear for a nullable type. The null is encoded as the corresponding null wire value for the type's integer mode.
The null type itself does not produce any bytes independently — it only appears through nullable types.
Opt #
Encoding: int[0...N-1]
An option selects one value from a fixed set of named options. It is encoded as an integer with bounds int[0...N-1] where N is the number of options.
The value is the zero-based index of the selected option.
| Schema | Value | Encoding (hex) |
|---|---|---|
opt{"a","b","c","d"} |
"b" (index 1) |
01 |
Blob #
Encoding: int[max(0, a)...b] + raw bytes
A blob is a sequence of raw bytes. It is encoded as:
- An integer length prefix with bounds derived from the blob's content bounds.
- The raw bytes themselves.
The blob's schema bounds [a:b] (or [:b] where a defaults to 0) define the minimum and maximum byte length of the content. The length prefix integer is int[max(0,a)...b]:
- Both bounds present → unsigned mode:
f(e) = max(0,a) + e - The decoded value
f(e)is the content length, which must satisfymax(0,a) ≤ f(e) ≤ b - The encoded wire value is
e = actual_length - max(0,a)
| Schema | Content length | Wire (e) | Encoding (hex) |
|---|---|---|---|
blob[:20] |
2 bytes | 2 |
02 [2 bytes] |
blob[4:20] |
5 bytes | 1 |
01 [5 bytes] (5 - 4 = 1) |
Constraint: Blob must have an upper bound defined (b).
String #
Encoding: int[max(0, a)...b] + UTF-8 bytes
A string is UTF-8 encoded text. It follows the exact same encoding as blob: an integer length prefix followed by the UTF-8 bytes.
The length is the byte length of the UTF-8 encoding (not character count).
| Schema | Value | Byte length | Wire (e) | Encoding (hex) |
|---|---|---|---|---|
string[:30] |
"Alice" |
5 | 5 |
05 41 6C 69 63 65 |
string[3:30] |
"Alice" |
5 | 2 |
02 41 6C 69 63 65 (5 - 3 = 2) |
Constraint: String must have an upper bound defined (b).
Ref #
Encoding: determined by the referenced schema's root type
A reference points to another schema. The encoded value is produced by encoding the reference's data according to the root type of the referenced schema.
There is no length prefix — the referenced schema's root type determines how many bytes the ref consumes.
Example: ref(abc123def...) → look up schema with ID abc123def...
→ encode ref data using that schema's root type
Validation: If there is an infinite loop of references (schema A references schema B which references schema A), the schema is invalid.
Array #
Encoding: int[max(0, a)...b] + concatenated elements
An array is an ordered list of elements of the same type. It is encoded as:
- An integer element count with bounds
int[max(0, a)...b]whereais the minimum andbthe maximum element count. - Each element encoded recursively, concatenated in order.
| Schema | Elements | Count | Encoding (hex) |
|---|---|---|---|
array{int}[:3] |
[10, 20] |
2 | 02 0A 14 |
Constraint: Array must have an upper bound defined (b).
Elements have no separator or length prefix between them — each element's encoding is self-delimiting (either fixed-size or length-prefixed).
Struct #
Encoding: concatenated fields sorted by field name
A structure is an ordered collection of named fields. Fields are sorted before encoding:
- Shorter field names first.
- Same-length names sorted lexicographically by UTF-8 big-endian byte value.
This sorting is the same as the canonicalization sorting used for schema IDs. The order in the schema file does not affect encoding.
There is no length prefix — the schema defines exactly which fields exist and their types.
| Schema | Encoding (hex) |
|---|---|
struct{y: int, x: int} with x=10, y=20 |
0A 14 |
In this example, x and y are both 1 byte. x (0x78) < y (0x79), so x (10 = 0x0A) is encoded before y (20 = 0x14), even though the schema declares y first.
Constraint: struct cannot be nullable.
Nullable Types (?) #
When a type is declared as nullable (prefix ?), the encoding changes:
- If the value is null: encode the corresponding null integer for the underlying integer mode.
- If the value is present: encode normally, but apply the null offset to the integer portion.
For length-prefixed types (string, blob), null is encoded as the null integer value with no trailing data. The decoder reads the null integer and knows not to expect any content bytes.
| Schema | Value | Encoding (hex) | Notes |
|---|---|---|---|
?string[:300] |
null | 00 |
unsigned null |
?string[:300] |
"hi" |
03 68 69 |
length=2, offset: wire=(2-0)+1=3 |
?int |
null | 7F |
two's comp null |
?int |
-5 |
7A |
wire = -5 - 1 = -6 |