Skip to content
Engine

Data

Serde

Compact binary serialization with schemas. Turn tables into small buffers for networking and storage, and back.

PackageShared

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): any
A player snapshot
local 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

TypeSizeValues
Serde.UInt8(), UInt16(), UInt32()1, 2, 4 bytesUnsigned integers.
Serde.Int8(), Int16(), Int32()1, 2, 4 bytesSigned integers.
Serde.Float32(), Float64()4, 8 bytesDecimal numbers.
Serde.VarInt()1 to 10 bytesNon-negative integers; small numbers take fewer bytes.
Serde.VarSInt()1 to 10 bytesIntegers that may be negative; small magnitudes take fewer bytes.
Serde.Boolean()1 bytetrue / false.

Text and bytes

Serde.String(maxLength: number?): Type
Serde.Buffer(maxLength: number?): Type

Length-prefixed strings and buffers. Encoding or decoding more than maxLength bytes errors; the default comes from configuration.

Roblox types

TypeEncoding
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): Type

The value or nil, at the cost of one byte.

Array

Serde.Array(element: Type, maxLength: number?): Type

A list where every item has the same type.

Map

Serde.Map(keyType: Type, valueType: Type, maxEntries: number?): Type

A dictionary with typed keys and values.

Tuple

Serde.Tuple(...: Type): Type

A 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 }): Schema

A 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 }): Type

One 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 }): Type

Packs 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 wire

Dynamic

Serde.Dynamic(): Type

Encodes 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 }): Type

Writes 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?): BitReader

Write and read individual bits for custom formats.

BitWriterBitReader
: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(): bufferPosition, 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): number

Maps 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

Defaults from DefaultConfiguration/Packages/Serde6 settings
SettingDefaultWhat it does
DefaultMaxStringLength32768Max bytes Serde.String() encodes/decodes when no length is passed.
DefaultMaxBufferLength65536Max bytes Serde.Buffer() encodes/decodes when no length is passed.
DefaultMaxArrayLength4096Max entries Serde.Array() encodes/decodes when no length is passed.
DefaultMaxMapEntries4096Max entries Serde.Map() encodes/decodes when no count is passed.
MaxDepth64Nesting depth any type may recurse into while encoding/decoding.
DefaultCFrameAngleBits12Bits 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