Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reference

This page provides an overview of the cbor_serialization API - for details, see the API reference.

Parsing

Common API

CBOR parsing uses the common serialization API, supporting both object-based and dynamic CBOR data item:

type
  NimServer = object
    name: string
    port: int

  MixedServer = object
    name: CborValueRef
    port: int

  RawServer = object
    name: CborBytes
    port: CborBytes

let rawCbor = Cbor.encode(NimServer(name: "localhost", port: 42))

# decode into native Nim
let native = Cbor.decode(rawCbor, NimServer)

# decode into mixed Nim + CborValueRef
let mixed = Cbor.decode(rawCbor, MixedServer)

# decode any value into nested cbor raw
let raw = Cbor.decode(rawCbor, RawServer)

# decode any valid CBOR, using the `cbor_serialization` node type
let value = Cbor.decode(rawCbor, CborValueRef)

Standalone Reader

A reader can be created from any faststreams-compatible stream:

var reader = Cbor.Reader.init(memoryInput(rawCbor))
let native2 = reader.readValue(NimServer)

# Overwrite an existing instance
var reader2 = Cbor.Reader.init(memoryInput(rawCbor))
var native3: NimServer
reader2.readValue(native3)

Parser options

Parser options allow you to control the limits of the parser. Set them by passing to Cbor.decode or when initializing the reader:

  rawCbor, NimServer, conf = defaultCborReaderConf(nestedDepthLimit: 0))

Flavors can be used to override the defaults for some these options.

Limits

Parser limits are passed to decode, similar to flags:

You can adjust these defaults to suit your needs:

  • nestedDepthLimit [=512]: Maximum nesting depth for objects and arrays (0 = unlimited).
  • arrayElementsLimit [=0]: Maximum number of array elements (0 = unlimited).
  • objectFieldsLimit [=0]: Maximum number of key-value pairs in an object (0 = unlimited).
  • stringLengthLimit [=0]: Maximum string length in bytes (0 = unlimited).
  • byteStringLengthLimit [=0]: Maximum byte string length in bytes (0 = unlimited).
  • bigNumBytesLimit [=64]: Maximum number of BigNum bytes (0 = unlimited).

Special types

  • CborBytes: Holds a CBOR value as a distinct seq[byte].
  • CborVoid: Skips a valid CBOR value.
  • CborNumber: Holds a CBOR number.
    • Use toInt(n: CborNumber, T: type SomeInteger): Opt[T] to convert it to an integer.
    • The integer field for negative numbers is set to abs(value)-1 as per the CBOR spec. This allows to hold a negative uint64.high value.
  • CborValueRef: Holds any valid CBOR value, it uses CborNumber instead of int.

Writing

Common API

Similar to parsing, the common serialization API is used to produce CBOR data items.

# Convert object to cbor raw
let blob = Cbor.encode(native)

Standalone Writer

var output = memoryOutput()
var writer = Cbor.Writer.init(output)
writer.writeValue(native)
let decoded = Cbor.decode(output.getOutput(seq[byte]), NimServer)
echo decoded

Flavors

Flags and limits are runtime configurations, while a flavor is a compile-time mechanism to prevent conflicts between custom serializers for the same type. For example, a CBOR-RPC-based API might require that numbers are formatted as hex strings while the same type exposed through REST should use a number.

Flavors ensure the compiler selects the correct serializer for each subsystem. Use defaultSerialization to assign serializers of a flavor to a specific type.

# Parameters for `createCborFlavor`:

  FlavorName: untyped
  mimeTypeValue = "application/cbor"
  automaticObjectSerialization = false
  automaticPrimitivesSerialization = true
  requireAllFields = true
  omitOptionalFields = true
  allowUnknownFields = true
  skipNullFields = false
type
  OptionalFields = object
    one: Opt[string]
    two: Option[int]

createCborFlavor OptCbor
OptCbor.defaultSerialization OptionalFields
  • automaticObjectSerialization: enable automatic serialization for all object types.
  • automaticPrimitivesSerialization: enable automatic serialization for all primitive types.
  • allowUnknownFields: Skip unknown fields instead of raising an error.
  • requireAllFields: Raise an error if any required field is missing.
  • omitOptionalFields: Writer ignores fields with null values.
  • skipNullFields: Reader ignores fields with null values.

Custom parsers and writers

Parsing and writing can be customized by providing overloads for the readValue and writeValue functions. Overrides are commonly used with a flavor that prevents automatic serialization, to avoid that some types use the default serialization, should an import be forgotten.

# Custom serializers for MyType should match the following signatures
proc readValue*(r: var Cbor.Reader, v: var MyType) {.raises: [IOError, SerializationError].}
proc writeValue*(w: var Cbor.Writer, v: MyType) {.raises: [IOError].}

# When flavors are used, use the flavor reader/writer instead
proc readValue*(r: var MyFlavor.Reader, v: var MyType) {.raises: [IOError, SerializationError].}
proc writeValue*(w: var MyFlavor.Writer, v: MyType) {.raises: [IOError].}

Objects

Decode objects using the parseObject template. To parse values, use helper functions or readValue. The readObject and readObjectFields iterators are also useful for custom object parsers.

proc readValue*(r: var Cbor.Reader, table: var Table[string, int]) =
  parseObject(r, key):
    table[key] = r.parseInt(int)

Sets and List-like Types

Sets and list/array-like structures can be parsed using the parseArray template, which supports both indexed and non-indexed forms.

Built-in readValue implementations exist for regular seq and array. For set or set-like types, you must provide your own implementation.

type
  HoldArray = object
    data: array[3, int]

  HoldSeq = object
    data: seq[int]

  WelderFlag = enum
    TIG
    MIG
    MMA

  Welder = object
    flags: set[WelderFlag]

proc readValue*(r: var Cbor.Reader, value: var HoldArray) =
  # parseArray with index, `i` can be any valid identifier
  r.parseArray(i):
    value.data[i] = r.parseInt(int)

proc readValue*(r: var Cbor.Reader, value: var HoldSeq) =
  # parseArray without index
  r.parseArray:
    let lastPos = value.data.len
    value.data.setLen(lastPos + 1)
    readValue(r, value.data[lastPos])

proc readValue*(r: var Cbor.Reader, value: var Welder) =
  # populating set also okay
  r.parseArray:
    value.flags.incl r.parseInt(int).WelderFlag

Custom Iterators

Custom iterators provide access to sub-token elements:

customIntValueIt(r: var CborReader; body: untyped)
customNumberValueIt(r: var CborReader; body: untyped)
customStringValueIt(r: var CborReader; limit: untyped; body: untyped)
customStringValueIt(r: var CborReader; body: untyped)

Convenience Iterators

readArray(r: var CborReader, ElemType: typedesc): ElemType
readObjectFields(r: var CborReader, KeyType: type): KeyType
readObjectFields(r: var CborReader): string
readObject(r: var CborReader, KeyType: type, ValueType: type): (KeyType, ValueType)

CborReader Helper Procedures

See the API reference

CborWriter Helper Procedures

See the API reference

Enums

type
  Fruit = enum
    Apple = "Apple"
    Banana = "Banana"

  Drawer = enum
    One
    Two

  Number = enum
    Three = 3
    Four = 4

  Mixed = enum
    Six = 6
    Seven = "Seven"

cbor_serialization automatically detects the expected representation for each enum based on its declaration.

  • Fruit expects string literals.
  • Drawer and Number expect numeric literals.
  • Mixed (with both string and numeric values) is disallowed by default. If the CBOR value does not match the expected style, an exception is raised. You can configure individual enum types:
configureCborDeserialization(
    T: type[enum], allowNumericRepr: static[bool] = false,
    stringNormalizer: static[proc(s: string): string] = strictNormalize)

# Example:
Mixed.configureCborDeserialization(allowNumericRepr = true) # Only at top level

You can also configure enum encoding at the flavor or type level:

type
  EnumRepresentation* = enum
    EnumAsString
    EnumAsNumber
    EnumAsStringifiedNumber

# Examples:

# Flavor level
Cbor.flavorEnumRep(EnumAsString)   # Default flavor, can be called from non-top level
Flavor.flavorEnumRep(EnumAsNumber) # Custom flavor, can be called from non-top level

# Individual enum type, regardless of flavor
Fruit.configureCborSerialization(EnumAsNumber) # Only at top level

# Individual enum type for a specific flavor
MyCbor.flavorEnumRep(Drawer, EnumAsString) # Only at top level