JSON, CBOR and numeric types
2026-04-20 14:05
CBOR makes sense as a canonical representation format as that’s very hard to do with JSON (search for “canonical JSON” and you’ll find a lot of prior art). But having a JSON representation often makes sense, e.g. at HTTP API boundaries. Sending the data in a human readable string format almost every programming language natively supports is a big win. While the JSON doesn’t necessarily needs to be canonical, the data must survive a full CBOR -> JSON -> CBOR round-trip. One core problem are numeric types and this is exactly what this blog post is about.
Numbers in CBOR
We restrict the CBOR to a subset called DAG-CBOR/DRISL. The supported numeric types are integers and floats. Integers must always be encoded in the smallest representation possible (8, 16, 32 and 64-bits are supported). Floats are always in IEEE 754 double-precision binary floating-point format.
Numbers in JSON
Converting from JSON to CBOR is similar to converting it into the native types of a programming language. There is the same problem of having only a single numeric type in JSON. But most programming languages follow the same convention to distinguish between floats and integers. If the number consists of digits only, it’s considered an integer, else it’s a float.
We can use the same convention going from JSON to CBOR as well. Suddenly we have support for integers and floats in JSON.
Numbers in JavaScript
When converting between JSON and CBOR, there’s usually an intermediate step in your favourite programming language that converts it to its native types. So most of the time it’s really JSON -> programming language native types -> CBOR and vice versa.
In most languages that’s not a problem, as their native types also have support for integers and floats. The elephant in the room is JavaScript. Historically there’s only a single numeric type, which is a float. So a naive JSON -> JSON.parse() -> CBOR approach would fail, as all numbers would end up as floats in CBOR.
There a two ways out of this. One is to use JavaScript’s BigInt type that was introduced in 2020. You would use BigInt for integers and then use the native JavaScript number type for floats. For JSON parsing a [custom reviver] would need to be used that outputs a BigInt for every number that only consists of digits:
const reviver = (_, value, context) => {
// When it's a number and the original text is digits only.
if (typeof value === 'number' && /^\d+$/.test(context.source)) {
return BigInt(value)
}
return value
}
For encoding it as CBOR again, you would need a CBOR library that treats BigInts as CBOR integers and encodes them into their minimal representation and encodes all native numbers as 64-bit floats. That library would then also be responsible for CBOR -> native JavaScript types conversion using BigInts.
For native Javascript types to JSON, you would need to make sure that floats are always represented with a decimal point. By default a number like 2.0 becomes just 2. This can be done with a custom replacer:
const replacer = (_, value) => {
if (typeof value === 'bigint') {
return JSON.rawJSON(value.toString())
}
if (typeof value === 'number') {
// NaN / Infinity are not valid JSON.
if (!Number.isFinite(value)) {
return null
}
// Force a decimal point even when the value is mathematically integral.
if (Number.isInteger(value)) {
return JSON.rawJSON(`${value}.0`)
}
}
return value
}
Another way, which might be simpler, is to skip the native JavaScript object part and use a custom JSON tokenizer that then creates the CBOR directly (and vice versa). cborg can be used for that.
Conclusion
It’s possible to have a well defined, clean round-trip between JSON and CBOR for numeric types. Integers and floats can be distinguished in JSON by treating digit-only numbers as integers and all others as floats. No additional schema information is needed.