Data Types
Delta-Pack schemas are built from a fixed set of data types, each with a defined snapshot encoding, diff encoding, and parsing rules. Every type is available across all language implementations.
Types fall into three groups — primitives, containers, and named types — described below.
Primitives
Primitives represent the basic scalar values:
| Type | YAML Syntax | JSON Example | Encode | Diff |
|---|---|---|---|---|
| String | string | "hello" | UTF-8 with per-message dictionary compression | New value (old value pre-loaded in dictionary) |
| Int | int | 42, -7 | ZigZag varint | New value |
| Int (bounded) | int(min=0, max=100) | 50 | Bit-packed in log₂(max − min + 1) bits | New value |
| Uint | uint | 42 | Varint (shorthand for int(min=0)) | New value |
| Float | float | 3.14 | IEEE 754 32-bit | New value |
| Float (quantized) | float(precision=0.01) | 3.14 | round(value / precision) as ZigZag varint | New value |
| Boolean | boolean | true | Single bit (RLE-compressed) | Change bit (decoder flips old value) |
Containers
Containers wrap other types:
| Type | YAML Schema | JSON Example | Encode | Diff |
|---|---|---|---|---|
| Array | int[] | [1, 2, 3] | Length prefix + elements in sequence | New length + sparse updates (index + element diff) + appended elements |
| Map | <string, int> | {"a": 1, "b": 2} | Length prefix + key-value pairs | Positional deletions + positional updates (with value diffs) + keyed additions |
| Optional | string? | "value" or null | Presence bit + value if present | Was null: new value directly; was non-null: presence bit + value diff |
Named Types
Named types are the only types that can be directly encoded/decoded or used as the codegen --type:
| Type | YAML Schema | JSON Example | Encode | Diff |
|---|---|---|---|---|
| Object | Position:x: floaty: float | {"x": 1.5, "y": 2.0} | Fields in declaration order | Change bit + per-field change bits with diffs |
| Enum | Team:- RED- BLUE- GREEN | "RED" | log₂(variant count) bits | New value |
| Union | Contact:- EmailContact- PhoneContact | {"EmailContact": {"email": "..."}} | Variant bits + variant fields | Same type: per-field diffs; new type: variant bits + full encode |
| Type alias | UserId: string | "abc123" | Resolved to underlying type | Resolved to underlying type |
Language bindings
How each schema type is represented in TypeScript, C#, and Rust:
| Schema type | TypeScript | C# | Rust |
|---|---|---|---|
string | string | string | String |
int, int(min,max) | number | long | i64 |
uint | number | long | u64 |
float, float(p) | number | float | f32 |
boolean | boolean | bool | bool |
T[] | T[] | List<T> | Vec<T> |
T? | T | undefined | T? | Option<T> |
<K, V> | Map<K, V> | OrderedDictionary<K, V> | IndexMap<K, V> |
| Object | type (structural) | class | struct |
| Enum | string literal union | enum | enum |
| Union | discriminated union (_type) | abstract class + variant subclasses | enum (tagged) |
| Type alias | resolved to underlying type | resolved to underlying type | resolved to underlying type |
Binary layout
Every encoded message shares the same structure:
[data section][RLE bits][bit count (reverse varint)]The data section holds whole-byte values (strings, varints, floats). The RLE section holds packed bits (booleans, enums, bounded integers, and change flags). The bit count at the end lets the decoder find the boundary.
Diff compactness comes from change bits: inside objects, unions, arrays, and maps, each field or element is preceded by a single bit in the RLE section. If it's 0, nothing else is encoded for that field — the decoder keeps the old value.