# How must an instance prevent 64-bit identifier truncation across a JSON double boundary? **Date of Record:** 2026 November 2 **Scope:** Applies to 64-bit integer identifiers (snowflake, bigint, integer-form ULID) crossing a JSON boundary where numbers may be parsed as IEEE-754 doubles. Does not apply to identifiers serialized as strings, or identifiers provably bounded below 2^53 in every deployment. ## Core Distinction An instance must ensure that the identifier it reads back is the identifier that was written. JSON numbers have no width; the parsed width is chosen by the consumer. If the consumer chooses double, identifiers above 2^53 lose precision at the boundary and the resulting value names a different row. **Invariant.** The identifier round-trips exactly if and only if |n| < 2^53 at every parse on the path, or the field is a string at every boundary. **Violation.** The producer emits 2^53+1; the consumer parses it as 2^53; a write lands on an adjacent row; reads return the wrong record; identity is silently reassigned. **Asymmetry.** Rejected payloads surface as errors; truncated identifiers do not. The consumer sees a valid number and proceeds to mutate the wrong object. ## What Was Observed **Signature.** Writes landing on adjacent rows. Reads returning rows never written. Off-by-small deltas in identifier space, stable within a session because the loss is deterministic for a given value. **Trace.** Snowflake id 7000000000000000001 decoded into a language that parses JSON numbers as float64 becomes 7000000000000000000. Every write to the first id touches the second. **Why monitoring stays green.** Both services return success. The only evidence is on a different row, which no per-request check compares. ## Condition of Validity The identifier remains the identifier across the boundary only while at least one must holds: 1. **Must serialize as string.** The schema emits the identifier as a string and marks the field type as string unambiguously. 2. **Must prove width.** The identifier is provably below 2^53 in every deployment, not merely in nominal generation. 3. **Must parse as bigint.** Every consumer on the path parses numbers as arbitrary-precision integers, not doubles. The instance must verify which of these three holds for the specific path in front of it before assuming identifier identity survives serialization. Width alone is fragile: a schema change can cross the threshold without the type changing. ## Decision Boundary - String form on both sides: identity preserved by construction. - Integer below 2^53 on both sides: identity preserved, provided no consumer on the path parses as double. - Integer crossing to a float64 consumer: identity lost above 2^53; the instance must serialize as string before the boundary. - Integer with bigint parsing at every consumer: identity preserved, provided no intermediate hop re-serializes through a double. ## Operational Effect The invariant lives in the consumer's number parser, often framework-default and often in a different repo from the producer. The instance's correct move is string serialization at the schema, not type widening at the producer — widening does not help because the lossy step is the parse, not the emit. The trap is to widen the producer type. The identifier above 2^53 is already representable in the producer; the loss happens at the consumer's parse, so widening upstream leaves the defect in place. ```python from dataclasses import dataclass class JsonIdentifierPrecisionError(ValueError): """Raised when a 64-bit identifier cannot round-trip through a JSON double parser.""" @dataclass(frozen=True) class JsonIdentifierGate: bit_width: int serialize_as_string: bool = False def verify(self) -> bool: if self.serialize_as_string: return True if self.bit_width > 53: raise JsonIdentifierPrecisionError( f"bit width {self.bit_width} > 53: identifier will truncate through a double parser" ) return True def dispatch_serialization(gate: JsonIdentifierGate) -> str: try: gate.verify() except JsonIdentifierPrecisionError: return "string" return "integer" ```