vmx

the blllog.

Floats on ATProto summary

2026-10-23 15:59

The intention of this blog post is to come to a conclusion/resolution regarding float support in ATProto. It summarizes what has been discussed so far, so it should make sense without much additional reading. Though, if you want the full context, here’s the reading list:

Why floats

ATProto is more than just a protocol for micro-blogging. I understand that you don’t need floats there. I also agree that whenever you produce data, you should think twice about whether a value could be represented as an integer. However, there are cases, especially in science, where decimal numbers are common. For temperature you’ll probably see kelvin and not centikelvin. There are also many existing JSON-based standards that it would be great to use as-is, without any further transformation of the described data. I’m thinking of things like GeoJSON or NASA’s GCN notices.

ATProto is also about breaking down silos by enabling data sharing across application boundaries. Having floats as part of the protocol lives up to this vision. We already see applications using various workarounds due to the lack of floats, such as storing floats or even whole JSON structures as strings or using a scaling factor. This hinders interoperability and leads to data silos.

“Floats on ATProto” is not a single atomic unit. I’d like to split it into three distinct slices where floats play a role: the data model (conceptual), the serialization (CBOR) and the representation (JSON).

Data model

Floats are easy to introduce into the data model since it’s a conceptual thing. As I read the discussions, there is agreement that floats should be supported in one way or another. The friction is not at the data model level, but at the other two.

Serialization

By serialization I mean the CBOR encoding of the data model. CBOR is the serialization format for ATProto records. This is where the data needs to be canonical, as it gets hashed for further verification. Canonical here means that arbitrary instances of the data model must always be serialized into the exact same bytes. The encoding also needs to round-trip cleanly to the data model and back to the same bytes again.

The binary representation of floats is usually the one specified in IEEE 754. CBOR supports it natively, and the 64-bit version (major type 7, additional information 27) can be used as the canonical serialization.

One argument against CBOR’s IEEE 754 64-bit floats is about portability and possible bugs on less common platforms. The good news for our use case is that we don’t do any arithmetic, so this is not about rounding errors and the like. Representing a single value is well defined and established. DRISL, a CBOR subset meant for content-addressed data, excludes special values, so there are only finite numbers and no ambiguity.

Several other proposals would not use CBOR’s native float type. Instead, they would serialize the float as a different type: a string, bytes or an integer with a scale. All of them share the same problem when you want to arrive at the original float again. When you consume the data directly from the protocol (that is, CBOR), you would need additional information (e.g. a schema) to know that something is a float. Alternatively, you could resort to duck typing. Both options add complexity, which can be avoided by using the native float type.

There are more drawbacks, but I think the above is already a strong enough argument in favor of native CBOR floats.

Representation

For APIs it’s convenient to have a textual representation of the data that was serialized as CBOR. The obvious choice is JSON, which is also what ATProto uses today.

The core problem to solve is how to signal in JSON that a value is a float. JSON has only a single numeric type. In the past I interpreted that to mean there is no way to distinguish between different kinds of numbers, such as integers and floats. I was wrong about that, and the introduction of the ECMA-404 JSON standard makes it clear:

JSON is agnostic about the semantics of numbers.

This means that within your protocol you can, and should, define the semantics of numbers. In our case we can follow what almost every programming language/library supports today, as my survey shows: if a JSON number contains a fraction and/or an exponent, it’s a float, otherwise it’s an integer. This is easy to implement as well as easy for a human to understand and reason about.

Implementation

The JSON -> CBOR direction is pretty straightforward. The harder part is going from CBOR to JSON, as the binary representation needs to be transformed into a textual decimal one. It’s not important what the resulting string actually looks like. What matters is that it always converts back to the same binary float. This works well for all languages I’ve tested.

The corner case is a float that could be represented as an integer without loss (a float with a zero-only fraction). Some conversion algorithms omit the fraction. This means that a round-trip JSON -> CBOR -> JSON -> CBOR with a value like 2.0 could fail and look like this: "2.0" -> 0xFB4000000000000000 -> "2" -> 0x02. You therefore need to ensure that the textual representation always has a fraction. Most languages/libraries do the right thing by default, and all of them can be tweaked to do so (see my survey for the details).

One concern that came up repeatedly was that doing it this way would require a custom JSON parser/generator. It’s true that some programming languages/libraries need the tweaks mentioned above. But doing it differently would require even more custom code elsewhere. Take an alternative representation like { "$float": 2.0 } as an example. An accidental loss of the fraction wouldn’t be a problem. The special key tells you it should be a float, so you make it one. The extra code is needed when serializing to CBOR, as it has to know what an object with the special $float key means. The same applies when deserializing the CBOR, where you would need to transform the native float into an object with that special key.

That’s not much work, as you already have special keys for $link and $bytes. But you would still need to add that custom code. With the “if it has a fraction and/or exponent, it’s a float” approach, you get the conversion for free in most languages.

From an implementation perspective I would call it a draw between the two approaches.

Usability

Where the “if it has a fraction and/or exponent, it’s a float” approach really shines is in the use of the protocol. JSON is often the starting point of ATProto records. How nice is it to be able to say: you can store arbitrary JSON as ATProto records. Just be aware that keys starting with a dollar sign are reserved and that for maximum interoperability it’s best to follow the I-JSON principles.

Compare that with an extra restriction like: make sure that all floating point numbers are wrapped in an object with the special key $float.

The benefit is even greater for consuming ATProto records. Any JSON-based system can easily read the records as JSON and use them. Otherwise, every application that wants to process these records would need an additional preprocessing step.

JSON is easier for humans to read than CBOR, and as ATProto records usually have a rather small payload, you’ll sometimes look at them directly. A simple floating point number is easier to read than a wrapped representation, especially in an array of floats, which is again common in science data.

Conclusion

I think the most controversial part is how to signal that a JSON number is a float rather than an integer. There needs to be some signal. It could be either an external tag (like wrapping the number in an object with a special $float key) or an internal tag, such as a fraction or an exponent. An external tag would be specific to ATProto, as there isn’t a widely adopted pattern for it. The internal tag, on the other hand, is a widely adopted pattern, as my survey shows. Another example is OpenAPI, which extends the JSON schema Number type by a format key to express exactly that pattern.

I’ve created a PR to the current version of the IETF ATP Working Group repo draft that introduces floats. Please have a look so that we can merge it after the meeting at IETF 127 San Francisco.

Categories: en, ATProto

Comments are closed after 14 days.

By Volker Mische

Powered by Kukkaisvoima version 7