encoding.cbor is an RFC 8949 Concise Binary Object Representation codec.
CBOR is a compact, schema-free binary format that supports the same value
model as JSON (numbers, strings, arrays, maps) plus byte strings, tagged
items, IEEE 754 floats at three widths, and a small set of "simple"
values (true, false, null, undefined). It is used by COSE/CWT
(IETF security stack), WebAuthn/FIDO2, the Matter smart-home protocol,
and many IoT stacks because messages are typically 30–60 % smaller than
JSON and parse without quoting/escaping.
Three layers of API are available:
encode[T]/decode[T]— comptime-driven generic API. Works on primitives, strings, arrays, maps, structs (with@[cbor: 'name'],@[skip],@[cbor_rename: 'snake_case']), enums,time.Time(auto-tagged), and any type implementingMarshaler/Unmarshaler.Packer/Unpacker— manual streaming API. Use when the schema isn't known at compile time, or when you need full control over tags, indefinite-length items and simple values.Valuesumtype — dynamic representation for round-tripping unknown payloads or inspecting tagged data.
Defaults follow RFC 8949 preferred serialisation (§4.2.2): floats
shrink to the shortest IEEE 754 width that preserves their value, and
every length argument uses the shortest encoding. Set
EncodeOpts.canonical = true to additionally sort map keys for
hash/signature stability (§4.2.1, deterministic encoding).
import encoding.cbor
import time
struct Person {
name string
age int
email ?string
birthday time.Time
}
fn main() {
bob := Person{
name: 'Bob'
age: 30
birthday: time.now()
}
bytes := cbor.encode[Person](bob, cbor.EncodeOpts{})!
// bytes is []u8 — wire-ready CBOR
back := cbor.decode[Person](bytes, cbor.DecodeOpts{})!
assert back.name == 'Bob'
}Optional fields (?T) encode as CBOR null when set to none. Enums
encode as their underlying integer.
struct Login {
user_name string @[cbor: 'u'] // emit/read key "u"
password string @[skip] // never serialise
remember bool @[cbor_rename: 'kebab-case'] // becomes "remember"
}The @[cbor_rename: '...'] attribute on a struct (not a field) applies
a global rename strategy — supported strategies: snake_case,
camelCase, PascalCase, kebab-case, SCREAMING_SNAKE_CASE.
Use this when the schema is dynamic or when you need access to CBOR features that don't map directly to V types (tags, indefinite-length strings, custom simple values):
import encoding.cbor
fn main() {
mut p := cbor.new_packer(cbor.EncodeOpts{})
p.pack_array_header(3)!
p.pack_uint(42)!
p.pack_text('hello')!
p.pack_bool(true)!
bytes := p.bytes()
mut u := cbor.new_unpacker(bytes, cbor.DecodeOpts{})
n := u.unpack_array_header()! // 3
a := u.unpack_uint()! // 42
b := u.unpack_text()! // 'hello'
c := u.unpack_bool()! // true
_ = n _ = a _ = b _ = c
}When the payload schema is unknown at compile time, decode into
cbor.Value and walk the sumtype:
import encoding.cbor
fn main() {
bytes := cbor.encode[map[string]int]({
'a': 1
'b': 2
}, cbor.EncodeOpts{})!
v := cbor.decode[cbor.Value](bytes, cbor.DecodeOpts{})!
if val := v.get('a') {
if i := val.as_int() {
assert i == 1
}
}
}Value covers every CBOR type: IntNum, FloatNum, Text, Bytes,
Array, Map, Tag, Bool, Null, Undefined, Simple. Re-encoding
a Value round-trips bit-for-bit when the source was already in
preferred form.
For types that need a custom on-wire representation, implement either side of the interface:
import encoding.cbor
struct Color {
r u8
g u8
b u8
}
pub fn (c Color) marshal_cbor(mut p cbor.Packer) ! {
p.pack_array_header(3)!
p.pack_uint(c.r)!
p.pack_uint(c.g)!
p.pack_uint(c.b)!
}
pub fn (mut c Color) unmarshal_cbor(mut u cbor.Unpacker) ! {
n := u.unpack_array_header()!
if n != 3 {
return error('Color expects 3 elements')
}
c.r = u8(u.unpack_uint()!)
c.g = u8(u.unpack_uint()!)
c.b = u8(u.unpack_uint()!)
}For hashing or signing, set canonical: true so that map keys are
sorted by length-then-lexicographic order (RFC 8949 §4.2.1):
import encoding.cbor
bytes := cbor.encode[map[string]int]({'b': 2, 'a': 1},
cbor.EncodeOpts{ canonical: true })!
// keys are emitted in the order "a", "b" regardless of input orderValues of type time.Time are serialised with tag 0 (RFC 3339 string)
on encode and accept either tag 0 or tag 1 (epoch seconds) on decode.
Custom tags can be emitted/read via pack_tag / unpack_tag or by
constructing a Value with cbor.new_tag(number, content).
The test suite (vlib/encoding/cbor/tests/) covers every vector from
RFC 8949 Appendix A, plus indefinite-length strings, depth limits,
malformed-input rejection, UTF-8 validation, canonical ordering, and
tagged time round-trips.
v test vlib/encoding/cbor/tests/