Data
Serde
Compact binary serialization with schemas. Turn tables into small buffers for networking and storage, and back.
Getting it
local Serde = Engine:GetPackage("Serde")Overview
Serde (serialize/deserialize) turns values into a compact buffer according to a schema, and turns the buffer back into the same values. Because both sides know the schema, field names and types are not written, so the data is a fraction of the size of JSON. Use it for network packets, DataStore payloads, replays, or anything where bytes matter.
Every type, simple or composed, has the same two methods:
type:Encode(value): buffer
type:Decode(buf: buffer): anylocal Serde = Engine:GetPackage("Serde")
local Snapshot = Serde.Struct({
UserId = Serde.VarInt(),
Position = Serde.Vector3(),
Health = Serde.UInt8(),
Crouching = Serde.Boolean(),
Weapon = Serde.Optional(Serde.String(32)),
})
local packet = Snapshot:Encode({
UserId = 123456,
Position = Vector3.new(10, 5, -20),
Health = 87,
Crouching = false,
Weapon = "Rifle",
})
print(buffer.len(packet)) -- a few dozen bytes
local decoded = Snapshot:Decode(packet)Encoding checks every value against the schema and raises an error with the exact path that is wrong, such as Engine: Serde - value.Health: expected number, got string. Decoding checks lengths against limits, so a malicious buffer from a client cannot make the server allocate huge tables.
Numbers and booleans
| Type | Size | Values |
|---|---|---|
Serde.UInt8(), UInt16(), UInt32() | 1, 2, 4 bytes | Unsigned integers. |
Serde.Int8(), Int16(), Int32() | 1, 2, 4 bytes | Signed integers. |
Serde.Float32(), Float64() | 4, 8 bytes | Decimal numbers. |
Serde.VarInt() | 1 to 10 bytes | Non-negative integers; small numbers take fewer bytes. |
Serde.VarSInt() | 1 to 10 bytes | Integers that may be negative; small magnitudes take fewer bytes. |
Serde.Boolean() | 1 byte | true / false. |
Text and bytes
Serde.String(maxLength: number?): Type
Serde.Buffer(maxLength: number?): TypeLength-prefixed strings and buffers. Encoding or decoding more than maxLength bytes errors; the default comes from configuration.
Roblox types
| Type | Encoding |
|---|---|
Serde.Vector2() | Two 32-bit floats. |
Serde.Vector3() | Three 32-bit floats. |
Serde.Color3() | Three bytes (0-255 per channel). |
Serde.CFrame(angleBits?) | Position as three floats plus rotation packed into angleBits bits per axis (default 12, about 0.09 degrees). |
Serde.EnumItem(enumType?) | With an enum type: the item's value in 2 bytes. Without: the enum name plus the value, so any enum works. |
Composite types
Optional
Serde.Optional(inner: Type): TypeThe value or nil, at the cost of one byte.
Array
Serde.Array(element: Type, maxLength: number?): TypeA list where every item has the same type.
Map
Serde.Map(keyType: Type, valueType: Type, maxEntries: number?): TypeA dictionary with typed keys and values.
Tuple
Serde.Tuple(...: Type): TypeA fixed-length array where each position has its own type: Serde.Tuple(Serde.String(), Serde.UInt8()) encodes { "Sword", 3 }.
Struct / Schema
Serde.Struct(fields: { [string]: Type }): Type
Serde.Schema(def: { [string]: Type }): SchemaA table with named fields. Fields are written in alphabetical order, so the order you list them in does not matter. Schema is another name for Struct.
TaggedUnion
Serde.TaggedUnion(variants: { [number]: Type }): TypeOne of several shapes, chosen by a numeric tag. Values look like { Tag = number, Value = any }.
local Message = Serde.TaggedUnion({
[0] = Serde.Struct({ Text = Serde.String(200) }), -- chat
[1] = Serde.Struct({ Target = Serde.VarInt() }), -- trade request
})
local buf = Message:Encode({ Tag = 1, Value = { Target = 42 } })BitPack
Serde.BitPack(fields: { BitPackField }): TypePacks small numbers and flags into as few bits as possible. Each field is { Name, Bits, Signed?, Bool? } with Bits between 1 and 32 (Bool fields use 1 bit).
local Input = Serde.BitPack({
{ Name = "Forward", Bits = 1, Bool = true },
{ Name = "Jump", Bits = 1, Bool = true },
{ Name = "Lean", Bits = 3, Signed = true }, -- -4..3
{ Name = "Slot", Bits = 4 }, -- 0..15
})
-- 9 bits, so 2 bytes on the wireDynamic
Serde.Dynamic(): TypeEncodes any mix of nil, booleans, numbers, strings, arrays and string-keyed tables without a schema. Larger than a schema, but handy for free-form data.
Versioned
Serde.Versioned(schemasByVersion: { [number]: Type }, migrations: { [number]: (any) -> any }): TypeWrites the newest version number in front of the data. When decoding older data, it decodes with the old schema and runs each migration in turn (migrations[1] turns version 1 into version 2, and so on) until it reaches the newest shape. Every step between the oldest and newest version needs a migration.
local SaveData = Serde.Versioned({
[1] = Serde.Struct({ Coins = Serde.UInt32() }),
[2] = Serde.Struct({ Coins = Serde.UInt32(), Gems = Serde.UInt32() }),
}, {
[1] = function(old)
return { Coins = old.Coins, Gems = 0 }
end,
})Bit-level tools
BitWriter / BitReader
Serde.BitWriter(): BitWriter
Serde.BitReader(buf: buffer, bitOffset: number?): BitReaderWrite and read individual bits for custom formats.
| BitWriter | BitReader |
|---|---|
:WriteBit(0 or 1) | :ReadBit(): number |
:WriteBool(value) | :ReadBool(): boolean |
:WriteUInt(value, bits) | :ReadUInt(bits): number |
:WriteInt(value, bits) | :ReadInt(bits): number |
:Align() to the next byte | :Align() to the next byte |
:Finish(): buffer | Position, Remaining (bits) |
BitLength, ByteLength |
QuantizeFloat / Dequantize
Serde.QuantizeFloat(v: number, min: number, max: number, bits: number): number
Serde.Dequantize(q: number, min: number, max: number, bits: number): numberMaps a number in [min, max] to an integer that fits in bits bits, and back. Values outside the range are clamped. Combine with BitPack to send, for example, a 0-100 health value in 7 bits.
Configuration
DefaultConfiguration/Packages/Serde6 settings| Setting | Default | What it does |
|---|---|---|
| DefaultMaxStringLength | 32768 | Max bytes Serde.String() encodes/decodes when no length is passed. |
| DefaultMaxBufferLength | 65536 | Max bytes Serde.Buffer() encodes/decodes when no length is passed. |
| DefaultMaxArrayLength | 4096 | Max entries Serde.Array() encodes/decodes when no length is passed. |
| DefaultMaxMapEntries | 4096 | Max entries Serde.Map() encodes/decodes when no count is passed. |
| MaxDepth | 64 | Nesting depth any type may recurse into while encoding/decoding. |
| DefaultCFrameAngleBits | 12 | Bits Serde.CFrame() packs per Euler axis (12 is ~0.09 degrees of precision). |
Types
export type Type
export type Schema = Type
export type BitPackField = { Name: string, Bits: number, Signed: boolean?, Bool: boolean? }
export type BitWriter
export type BitReader