Huny's Dev Blog
huny.dev

Go JSON Large Integer Precision: Preserve IDs with UseNumber and int64

Reproduce rounded IDs in Go JSON decoding. Compare UseNumber with typed int64 fields and validate 12 boundary cases, including overflow, missing IDs, and trailing data.

Hun Jang
Hun Jang Oct 30, 2026

When Go's encoding/json reads a large integer ID into map[string]any, the default float64 conversion can change its value. Decode known fields directly into int64; for dynamic objects, preserve the number literal with Decoder.UseNumber and validate its range. This article compares both paths with runnable code and defines which API inputs to reject.

A frosted lens and a clear protective channel illustrate lost and preserved numeric precision. This is a conceptual illustration, not a data flow diagram.

Written and documentation checked: 2026-10-30. Execution environment: Go 1.24.7, Linux amd64, standard-library encoding/json. These are not claimed results for the latest Go release, nor defaults for other JSON packages.

Why decoding a number can change an ID

The example input is {"id":9007199254740993}. The default path stores the number as float64. Casting it to int64 afterward cannot recover digits already lost. This is more than a log formatting issue: the program below checks the cast result too.

The official Go Unmarshal documentation describes the default type used for JSON numbers in interface values. RFC 8259 §6 identifies [-(2^53)+1, (2^53)-1] as the integer range where implementations using binary64 can agree exactly. This does not mean every integer outside that range is wrong. Representing a particular value exactly and safely handling an entire sequence of IDs are different requirements.

Start with whether the schema is known

Input and use case
Choice
Additional checks
The numeric ID field has a known schema
An int64 struct field
Missing values, null, positivity
A dynamic object mixes different field types
UseNumber, then Int64()
Type, syntax, range errors
Multiple consumers, including browsers, exchange large IDs
Consider defining IDs as strings in the API contract
Consistency across all producers and consumers

As documented for UseNumber, this defers interpreting the number. It does not automatically provide arbitrary-precision integer arithmetic. Do not discard errors from Int64(). A string-ID contract differs from the numeric-only parser below, so its validation code must change as well.

A runnable comparison with input validation

Save this as json_number.go and run go run json_number.go. No external modules or services are required. The parseID contract is “one object with an unquoted positive int64 ID.” It rejects exponent notation such as 1e3 too.

The second Decode checks for another value or invalid characters after the first object. This example validates a complete input string. Applying it directly to a network stream that stays open can wait for the end of input.

Actual output and boundary cases

The program above was executed while preparing this article and produced:

ID value or input shape
Expected result in this example
9007199254740993, 9223372036854775807
Accept as exact integers
9223372036854775808
Reject: outside int64 range
1.5, 1e3
Reject: violates the integer notation contract
A quoted number, null, missing field
Reject: not the required numeric type
0, -1
Reject: not positive
Two objects, or broken after an object
Reject: not exactly one JSON value

All 12 cases represented in the table were checked. The default path's rounding and the struct path's exact value were also checked separately. These are not benchmarks or tests of the user's servers, the latest Go release, or cross-browser transport.

Scope and remaining validation

  • 1e3 is a valid JSON number. This parser rejects it because Int64() fails when interpreting its text as an integer literal. Accepting every mathematically integral JSON notation requires a separate design. When handling integer strings, also check range errors from strconv.ParseInt.
  • An int64 struct field does not establish that an input was required. In a fresh struct, missing fields or null can leave zero behind. A pointer can identify an unset value, but distinguishing omission from explicit null requires additional state.
  • This example allows unknown fields and does not reject duplicate keys. Decide separately whether inputs such as {"id":1,"id":2} are acceptable. At authentication or signature boundaries, review differences between parsers as well.
  • For HTTP handling, impose body-size and read-time limits separately. Fixing numeric precision does not limit request resource use.
  • If an upstream component already rounded an ID through floating point, the receiver's UseNumber cannot restore it. For identifiers that are not used in calculations, consider a string contract from the point where they are first created.

한국어 버전: Go JSON에서 큰 정수 ID가 바뀌는 이유: UseNumber와 int64로 정밀도 지키기Go JSON에서 큰 정수 ID가 바뀌는 이유: UseNumber와 int64로 정밀도 지키기

추천 글