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.
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
1e3is a valid JSON number. This parser rejects it becauseInt64()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
int64struct field does not establish that an input was required. In a fresh struct, missing fields ornullcan leave zero behind. A pointer can identify an unset value, but distinguishing omission from explicitnullrequires 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
UseNumbercannot 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로 정밀도 지키기