A modern, memory-safe CBOR (Concise Binary Object Representation, RFC 8949) library for Swift.
- Memory-safe by construction. Decoding reads through
Span<UInt8>using only bounds-checked access — the library contains zerounsafecode and compiles under.strictMemorySafety(). - Complete RFC 8949 support. Every example in RFC 8949 Appendix A round-trips, including tags, indefinite-length items, half/single/double floats, and the full ±2⁶⁴ integer range.
- Deterministic encoding. Shortest-form integers and canonically ordered map keys (RFC 8949 §4.2) by default.
- Fast and lean. Benchmarked against other Swift CBOR libraries, with fewer allocations and lower instruction counts across decode and encode.
- Two ways to use it. Work with the
CBORvalue model directly, or bridge anyCodabletype throughCBOREncoder/CBORDecoder. - Optional Foundation. Foundation is only used to bridge
Data; it lives behind a package trait you can switch off to build with no Foundation dependency at all. Sendablethroughout, with a@nonexhaustivepublic API designed to evolve without source breaks.
- Swift 6.3+
- macOS 26 / iOS 26 / tvOS 26 / watchOS 26 / visionOS 26 (or any Linux with a Swift 6.3 toolchain)
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/brokenhandsio/swift-cbor.git", from: "0.0.1"),
],Then add the CBOR product to your target:
.target(
name: "MyTarget",
dependencies: [
.product(name: "CBOR", package: "swift-cbor"),
]
),CBOR.decode reads a single, complete data item and requires the entire input to
be consumed:
import CBOR
let value = try CBOR.decode([0x83, 0x01, 0x02, 0x03])
// value == .array([.unsignedInt(1), .unsignedInt(2), .unsignedInt(3)])You can decode from a [UInt8], an ArraySlice<UInt8>, or a Span<UInt8> directly.
let item: CBOR = ["temperature": 21, "unit": "C"]
let bytes = item.encode()
// Map keys are emitted in RFC 8949 deterministic order.CBOR conforms to the relevant ExpressibleBy*Literal protocols, so values read
naturally in source:
let message: CBOR = [
"id": 42,
"tags": ["a", "b"],
"payload": .byteString([0xde, 0xad, 0xbe, 0xef]),
"note": nil, // .null
]Read back with subscripts and typed accessors (each returns nil on a mismatch):
message["id"]?.int // 42
message["tags"]?[.unsignedInt(0)]?.string // "a"
message["payload"]?.bytes // [0xde, 0xad, 0xbe, 0xef]
message["note"]?.isNull // trueNegative integers follow the CBOR encoding, where .negativeInt(n) represents
-1 - n (so -1 is .negativeInt(0)). The int accessor and integer literals
hide this for you:
let alg: CBOR = -7 // .negativeInt(6)
alg.int // -7Bridge any Encodable / Decodable type, mirroring JSONEncoder / JSONDecoder:
struct Reading: Codable {
var sensor: String
var value: Double
var ok: Bool
}
let bytes = try CBOREncoder().encode(Reading(sensor: "abc", value: 1.5, ok: true))
let reading = try CBORDecoder().decode(Reading.self, from: bytes)Swift structs encode to CBOR maps keyed by their property names, in deterministic
key order. With the FoundationSupport trait enabled (the default), Data
properties encode as CBOR byte strings.
When a CBOR item is embedded in a larger buffer, decodeFirst returns the value
along with how many bytes it consumed — useful for slicing out whatever follows it:
let (value, consumed) = try CBOR.decodeFirst(buffer)
let remainder = buffer[consumed...]let options = CBOROptions(
maximumDepth: 16, // bound nesting on adversarial input (default 512)
rejectDuplicateMapKeys: true // throw on duplicate map keys (default false)
)
let value = try CBOR.decode(bytes, options: options)CBOROptions also controls deterministic encoding, which is on by default.
Tagged items use CBORTag, which comes with the common IANA tags:
let tagged: CBOR = .tagged(.uri, .textString("https://example.com"))
let custom: CBOR = .tagged(CBORTag(1668546817), .array([1, 2, 3]))Foundation is only linked to bridge Data in the Codable layer, and it is gated
behind the default-on FoundationSupport trait. To build with no Foundation
dependency, disable default traits:
.package(
url: "https://github.com/brokenhandsio/swift-cbor.git",
from: "0.0.1",
traits: [] // enable no traits, omitting the default FoundationSupport
),or, when building this package directly, from the command line:
swift build --disable-default-traits
swift test --disable-default-traitsWith the trait off, the entire value model, the Span-based parser, value-model
encode/decode, and the Codable bridge for all non-Data types are available with
no Foundation linked. (Data properties then encode via standard Codable as an
array of integers rather than a byte string.)
API documentation is built with DocC. The DocC plugin is gated behind an environment flag so consumers of the library never resolve it:
CBOR_DOCC=1 swift package generate-documentation --target CBORBenchmarks use ordo-one/benchmark and live
under Benchmarks/. They are likewise gated behind an environment flag so the
harness's dependency tree never reaches library consumers — a plain import CBOR
resolves with zero external dependencies:
CBOR_BENCHMARK=1 swift package benchmarkThey cover COSE-key decode/encode, large array/map throughput, full round-trips, and the Codable bridge. CI gates regressions on instruction count and allocation count — deterministic metrics — alongside peak resident memory. swift-cbor has been benchmarked against other Swift CBOR libraries and shows improvements across both allocation count and instruction count.
Like any Swift CBOR library built on a recursive value type, a decoded CBOR value is
deallocated recursively — freeing a value nested many thousands of levels deep can
exhaust the stack. Decoding and encoding themselves are fully iterative and never
recurse on input nesting, and CBOROptions.maximumDepth (default 512) bounds how deep
a decoded value can be, so values decoded from untrusted input free safely on ordinary
stacks. Lower maximumDepth if you decode on threads with unusually small stacks.
swift-cbor is available under the MIT license. See LICENSE for details.