Support sent over a ticket with a screenshot: an order confirmation showing ID 9007199254740993, and our admin tool returning "order not found" for it. Copy the ID out of the confirmation, paste it into the tool, nothing. Copy it from the database, it works. The two strings were not identical. The last digit differed by one.
Our order service generates snowflake-style 64 bit IDs and serialises them as JSON numbers. The notification service is Node, and JSON.parse turns every number into an IEEE 754 double. Doubles hold integers exactly up to 2^53 minus one, which is 9007199254740991. Above that, you get the nearest representable value. 9007199254740993 parses to 9007199254740992 and there is no error, no warning, and no way to detect it after the fact, because the value that arrives is a perfectly valid number.
This had been correct for three years because our IDs had been below 2^53. The timestamp component rolled past the boundary on a Tuesday and broke roughly one in several thousand lookups, which is exactly the rate that gets written off as user error.
The fix in the contract is the only one that holds: identifiers are strings. They are opaque tokens, nothing downstream does arithmetic on them, and a string survives every JSON parser ever written. We added "type": "string" with a digit pattern to the schema, shipped the producer emitting both id and a string order_id for two weeks, migrated consumers, then removed the number.
What I care more about is how we stopped the next one. The contract tests now include boundary fixtures on every numeric field: 2^53, 2^53 plus one, 2^63 minus one, and a negative. Consumers run those fixtures through their real deserialiser and assert the value round-trips, which would have failed this in CI years before the IDs got big. And our API guidelines now say plainly that any integer that can exceed 2^53 is serialised as a string, because JSON's number type has no width and every language picks its own.
An integration can be well formed, validated, and still lossy. Schema validation checks the shape of a value. It does not check that the receiver can hold it.
– Sergey Shinder
Top comments (0)