cbor-model adds CBOR serialization and CDDL schema generation to Pydantic
models.
pip install cbor-modelor with uv:
uv add cbor-modelFields are encoded as a CBOR map keyed by the integer or string supplied to
CBORField(key=...).
from typing import Annotated
from cbor_model import CBORModel, CBORField
class Sensor(CBORModel):
name: Annotated[str, CBORField(key=0)]
value: Annotated[float, CBORField(key=1)]
sensor = Sensor(name="temp", value=21.5)
data = sensor.model_dump_cbor() # a2006474656d7001fb4035800000000000
assert Sensor.model_validate_cbor(data) == sensorSwitch to array encoding by setting CBORConfig(encoding="array") and using
CBORField(index=...) — fields are serialized in index order.
from typing import Annotated
from cbor_model import CBORModel, CBORField, CBORConfig
class Point(CBORModel):
cbor_config = CBORConfig(encoding="array")
x: Annotated[int, CBORField(index=0)]
y: Annotated[int, CBORField(index=1)]
pt = Point(x=4, y=2)
data = pt.model_dump_cbor() # 820402
assert Point.model_validate_cbor(data) == ptWrap a field's value in a CBOR tag using CBORField(tag=...), or tag the entire
model with CBORConfig(tag=...).
from typing import Annotated
from cbor_model import CBORModel, CBORField, CBORConfig
class Reading(CBORModel):
cbor_config = CBORConfig(tag=40001)
sensor_id: Annotated[int, CBORField(key=0)]
raw: Annotated[bytes, CBORField(key=1, tag=40002)]Pass a CBORSerializationContext to control None and empty-collection exclusion:
from cbor_model import CBORSerializationContext
ctx = CBORSerializationContext(exclude_none=False, exclude_empty=False)
data = sensor.model_dump_cbor(context=ctx)Register encoders for types not natively supported by cbor2:
import decimal
from cbor_model import CBORConfig
class MyModel(CBORModel):
cbor_config = CBORConfig(
encoders={decimal.Decimal: lambda d: str(d)}
)
amount: Annotated[decimal.Decimal, CBORField(key=0)]Generate a CDDL schema from one or more models:
from cbor_model.cddl import CDDLGenerator
print(CDDLGenerator().generate(Sensor))
# sensor_name = 0
# sensor_value = 1
#
# Sensor = {
# ? sensor_name: tstr,
# ? sensor_value: float
# }Integer constraints are rendered as precise RFC 8610-compatible CDDL.
For example, lower-only bounds use numeric controls such as .gt or .ge,
and closed integer bounds are emitted as ranges:
from typing import Annotated
from pydantic import Field
from cbor_model import CBORField, CBORModel
from cbor_model.cddl import CDDLGenerator
class Packet(CBORModel):
count: Annotated[int, CBORField(key=0), Field(gt=0)]
code: Annotated[int, CBORField(key=1), Field(ge=0, le=255)]
print(CDDLGenerator().generate(Packet))
# packet_count = 0
# packet_code = 1
#
# Packet = {
# packet_count: int .gt 0,
# packet_code: 0..255
# }Map-encoded models always emit a per-model block of integer-key constants
(prefix is the model class name converted to snake_case) and reference
those constants in the map body. Use CBORField(description=...) to
attach a free-text comment that is rendered as ; <text> after the
field definition, and CBORField(override_name=...) to override the
identifier (used verbatim).
The package also exposes a small set of reusable integer aliases under
cbor_model.types:
from typing import Annotated
from cbor_model import CBORField, CBORModel, types
class Header(CBORModel):
version: Annotated[types.UInt1, CBORField(key=0)]
length: Annotated[types.UInt2, CBORField(key=1)]The currently available aliases are:
types.Int1: signed 8-bit integer (-128..127)types.UInt: unsigned integer (ge=0)types.UInt1: unsigned 8-bit integer (0..255)types.UInt2: unsigned 16-bit integer (0..65535)types.UInt4: unsigned 32-bit integer (0..4294967295)