# Type Encoding Every value is encoded recursively. The [integer](integer.md) 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](../schema.md#nullable-types). 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: 1. An **integer length prefix** with bounds derived from the blob's content bounds. 2. 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 satisfy `max(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](#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: 1. An **integer element count** with bounds `int[max(0, a)...b]` where `a` is the minimum and `b` the maximum element count. 2. 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: 1. Shorter field names first. 2. Same-length names sorted lexicographically by UTF-8 big-endian byte value. This sorting is the same as the [canonicalization sorting](../schema-id.md#3-canonicalize) 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](integer.md#null) 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 |