vmx

the blllog.

JSON, floats and native types

2026-06-29 15:26

JSON only has a [single numeric type], though I state in an earlier blog post that most programming languages distinguish between integer and floats when they encounter them. As this is a crucial piece in order to get native floats support on ATProto, I provide a survey across some languages and databases. The goal is to find the edge cases and workarounds where things break, and hopefully also show that floats in practice are not as bad as they are in theory.

The central idea is that floats and integers have a different shape, and this can be used to distinguish them in JSON. It’s not mentioned in any standard, but it is a convention that many programming languages and databases have adopted. A JSON number is considered an integer when it contains only digits and an optional sign. If it contains a decimal point and/or an e for exponential notation, it’s a float.

The survey repo

The json-floats-native-types repo contains an overview in the README on how various languages behave, as well as an implementation for each of them. While writing the code was assisted by AI, this is not AI slop. It wasn’t a few prompts and then calling it a day, but many days of thorough code review and refinement of the implementations and test fixtures. For languages that don’t have built-in JSON processing, I’ve tried to find the most popular libraries.

If you find cases where I process the JSON in an unidiomatic way, please let me know or open a pull request. While I’m familiar with most of the languages covered, I’m not with all of them. The selection is a bit arbitrary: it’s the languages for which some ATProto SDK exists. Though the list ended up covering widely used languages as well as some more obscure ones, to ensure things will work in those cases too. I’ve also included a less elaborate database section, as storing JSON in a database was brought up in the floats on ATProto thread.

Round-tripping CBOR

I want to show that for ATProto a CBOR -> JSON -> CBOR round-trip producing byte-identical CBOR is possible (using a CBOR subset called DAG-CBOR/DRISL). That said, I would argue that the ATProto protocol should never rely on such behaviour. At the API level it makes sense to have a one-time conversion from JSON to CBOR, but later JSON representations should only be used for display or processing purposes, not for integrity verification. When you need that, you should supply the original CBOR. This is also how I understand the ATProto data model spec that states in the JSON representation section: only DRISL-CBOR is used as a byte-reproducible representation.

Findings

The repo contains all the details and you can run it yourself online or locally. Here’s my interpretation of the results.

Decoding

Decoding is the conversion from a JSON string into native types. For all languages there is a way to distinguish between integers and floats. When that distinction is made, it’s always based on the heuristic mentioned above: integers are numbers consisting of digits only with an optional sign; all other numbers are floats.

The results fall into these categories:

  • Just works: C (jansson), C++ (nlohmann/json, RapidJSON), Dart (native), Elixir, Java (Jackson), PHP, PowerShell, Python, Ruby, Rust (serde_json), Swift, Zig
  • Needs introspection. The parsed number is somewhat wrapped, but the distinction can be made: C# (.NET), Go (json.Number), JavaScript, Kotlin (JVM)
  • No distinction possible: C (cJSON), Go

The need for introspection may sound like a blocker, but for ATProto it isn’t. There, JSON is just an intermediate format used to produce CBOR. That transformation already requires some custom implementation, as CBOR can contain bytes and CIDs. Neither of which is natively supported by JSON, but both expressed through convention.

Hence I consider those results sufficient indication that for decoding purposes, no special syntax is needed to distinguish floats from integers at the JSON level. The established convention can be relied upon, meaning many implementations won’t need a custom code path at all.

Encoding

Encoding is the conversion from a native language type into a JSON string. This part focuses on floats only, with a single integer type included to verify it’s preserved. In all languages it’s possible to maintain the distinction at the JSON level.

Rounding correctly to the next representable number across the normal/subnormal boundary works well, all languages handle it the expected way by default. The smallest positive subnormal is also encoded correctly by all implementations. Although the formatting differs slightly, it would result in the same bytes across all implementations, as the Round-trip decoding section shows.

Though there are differences, when encoding floats whose value fits without loss into an integer. Again we have three categories:

  • Just works: C (jansson), C++ (nlohmann/json, RapidJSON), Dart (native), Elixir, Java (Jackson), Kotlin (JVM), PowerShell, Python, Ruby, Rust (serde_json)
  • Needs a setting or wrapper type: C# (.NET), Go, Go (json.Number), JavaScript, PHP, Swift, Zig
  • Not possible: C (cJSON)

As with the decoding case, some languages require additional wrapper types. For most nothing special is needed, for others some custom handling is required. Since custom code for bytes and CIDs is already necessary anyway, this isn’t a significant burden.

Databases

Databases are somewhat special, since you typically don’t produce data directly, but the language driving the database does. The tests here are therefore a bit different: we check whether the float/integer distinction survives insertion and querying. PostgreSQL and SQLite were chosen as they are already used in the ATProto reference implementation. Apache CouchDB was included as a JSON-native database I’m familiar with.

Things work as expected, with one exception: the -0.0 case. With PostgreSQL’s jsonb type and Apache CouchDB, negative zero loses its sign and becomes positive, making it impossible to produce byte-identical CBOR from it.

The solution is to forbid -0.0 in the ATProto data model.

Conclusion

Most programming languages have a way to distinguish between floats and integers within JSON, and many have this built in. Sometimes tweaks or a different library are needed. Treating a JSON number that contains a fraction or exponent as a float is a well-established convention.

Categories: en, ATProto

Comments are closed after 14 days.

By Volker Mische

Powered by Kukkaisvoima version 7