Quick Reference
Keep it as a JSON number when: The value is a small integer, a percentage, a count, or anything where 15 significant digits is more precision than you'll ever need
Encode it as a JSON string instead when: The value is a large numeric ID, a financial amount that must stay exact to the last decimal, a high-precision coordinate, or any number where the 16th digit actually matters
You save a financial transaction with an amount of 1234567890.123456789. Five minutes later you look it up again, and the last few digits have quietly changed to something else. No exception was thrown. No warning appeared in your logs. JSON.parse() ran successfully and returned exactly what it was designed to return β just not what you sent it.
This is one of the most under-discussed bugs in modern web development, precisely because it doesn't look like a bug. The JSON is syntactically valid. The parser didn't crash. The API returned 200 OK. And yet, somewhere between serialization and deserialization, part of your number simply vanished.
If you work with financial amounts, large numeric IDs, geographic coordinates, or scientific measurements β or you're building a JSON API that other systems will consume β you've probably run into this already, or you will. This article walks through why it happens, how it shows up differently across languages, and what you can actually do about it.
1. The Problem: When Your Numbers Change Without an Error
Here are three situations where this bites developers in production:
Scenario 1: Financial Amounts Quietly Round
const raw = '{"amount": 1234567890.123456789}';
const parsed = JSON.parse(raw);
console.log(parsed.amount);
// 1234567890.1234567
// The last two digits are gone. No error. No warning.Scenario 2: Large Integer IDs Get Mangled
const raw = '{"orderId": 9007199254740993}';
const parsed = JSON.parse(raw);
console.log(parsed.orderId);
// 9007199254740992
// A different ID than the one that was sent -- off by exactly one.Scenario 3: Scientific or Geographic Coordinates Drift
const raw = '{"lat": 37.774929535897931, "lng": -122.419415602}';
const parsed = JSON.parse(raw);
console.log(parsed.lat);
// 37.77492953589793
// Close enough for a map pin, potentially not close enough for survey-grade data.In every case, the JSON itself was valid. The problem happened silently, during parsing, and nothing in the response told you it happened.
2. The Cause: IEEE 754 and the Digits Your Parser Can't Keep
2.1 The JSON Spec Doesn't Limit Precision β Your Parser Does
It's a common misconception that JSON itself restricts how many digits a number can have. It doesn't. The JSON specification defines numbers as an unbounded sequence of digits and leaves precision entirely up to the implementation reading them. In practice, almost every mainstream JSON parser β JavaScript's built-in JSON.parse, and the default number handling in many other languages' JSON libraries β represents every number as an IEEE 754 double-precision floating point value, because that's the native number type in those languages. The limit you're hitting isn't a JSON rule. It's a floating-point rule your parser inherited by default.
2.2 The IEEE 754 Double: About 15-17 Significant Digits, No More
An IEEE 754 double stores its mantissa in 52 bits, which works out to roughly 15 to 17 significant decimal digits of precision β 15 digits are always safely representable exactly, and up to 17 digits are recoverable in some cases, but anything beyond that gets rounded to the nearest value the format can actually hold. This isn't a bug in any particular parser; it's the defined behavior of the format itself.
| Digits in original number | What actually happens |
|---|---|
| 15 significant digits or fewer | Safe -- represented exactly by a double |
| 16-17 significant digits | Risky -- may round depending on the specific value |
| 18+ significant digits | Unsafe -- will be silently rounded to the nearest representable double |
2.3 Why There's No Error β By Design
Rounding to the nearest representable double is correct, defined behavior for IEEE 754 β it's not a failure state the parser can detect and reject on its own. As far as the parser is concerned, it did its job: it took a numeral and produced the closest double-precision value to it. The fact that this value differs from your input is invisible unless something explicitly checks for it before parsing happens β which almost nothing does by default.
3. The Solution: Choose the Right Representation, Per Language
3.1 When Native Numbers Are Genuinely Fine
Most numbers in most APIs never come close to 15 significant digits -- page counts, small integers, percentages, ordinary prices under a few million units. For these, native JSON numbers are simple, fast, and completely safe. Don't add complexity where there's no real risk.
3.2 When You Must Protect Precision
If a value is a large numeric ID (database primary keys, distributed-system IDs, some payment processor references), a financial amount that must stay exact, or a coordinate/measurement where every digit is meaningful -- encode it as a JSON string instead of a JSON number. A string is never rounded, no matter how many digits it holds.
3.3 Code Examples Across Languages
JavaScript / Node.js
// Native parsing silently rounds
const raw = '{"amount": 1234567890.123456789}';
const parsed = JSON.parse(raw);
console.log(parsed.amount);
// 1234567890.1234567 -- already wrong, no error thrown
// Fix 1: have the sender encode the value as a string
const safe = '{"amount": "1234567890.123456789"}';
const parsedSafe = JSON.parse(safe);
console.log(parsedSafe.amount);
// "1234567890.123456789" -- untouched
// Fix 2: use a reviver function with the source-text context
// (available in modern JS engines) to recover the exact literal
// before it gets rounded to a double
const recovered = JSON.parse(raw, (key, value, context) => {
if (key === 'amount' && context && context.source) {
return context.source; // the original numeral, as written
}
return value;
});
console.log(recovered.amount);
// "1234567890.123456789"Python
import json
from decimal import Decimal
raw = '{"amount": 1234567890.123456789}'
# Default: parsed as float, already rounded
data = json.loads(raw)
print(data['amount'])
# 1234567890.1234567
# Fix: parse decimal literals as Decimal instead of float
data_safe = json.loads(raw, parse_float=Decimal)
print(data_safe['amount'])
# Decimal('1234567890.123456789')Java
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.ToNumberPolicy;
Gson gson = new GsonBuilder()
.setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL)
.create();
// Numeric fields now deserialize to BigDecimal instead of double,
// preserving every digit present in the original JSON text.Go
package main
import (
"bytes"
"encoding/json"
"fmt"
)
func main() {
raw := []byte(`{"amount": 1234567890.123456789}`)
var data map[string]interface{}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber() // decode numbers as json.Number (string-backed) instead of float64
dec.Decode(&data)
fmt.Println(data["amount"]) // json.Number("1234567890.123456789") -- exact
}4. Common Pitfalls and How to Avoid Them
Pitfall #1: Assuming the JSON Spec Itself Caps Precision
It doesn't. The spec is silent on precision limits -- the ceiling you're hitting belongs to your specific parser's number type, not to JSON as a format. Two different JSON libraries can legally handle the exact same oversized number differently.
Pitfall #2: Round-Tripping Data Through a JavaScript Layer Silently Truncates It
Even if your database and your client both handle high-precision numbers correctly, if a Node.js service sits in the middle and does JSON.parse then re-serializes as part of a proxy or transformation step, the precision is lost at that hop -- even though neither endpoint has a bug.
// β A middleware layer that just passes JSON through JS objects
const body = JSON.parse(rawRequestBody); // rounds silently here
// ... some transformation ...
const forwarded = JSON.stringify(body); // the rounding is now permanent
// β
If the middleware doesn't need to read the numeric value,
// keep it as an opaque string/pass-through instead of parsing it
// into a JS number at allPitfall #3: Checking for Precision Loss After Parsing Is Already Too Late
By the time a value has become a JavaScript number or a language's native double, the original digits are already gone -- there's nothing left to inspect. Any precision check has to happen on the raw text, before JSON.parse (or your language's equivalent) ever runs.
Pitfall #4: Database Columns Can Introduce a Second, Independent Rounding Step
Even if your application code preserves precision perfectly, storing the value in a FLOAT or DOUBLE database column re-introduces the exact same problem at the storage layer. High-precision values need a DECIMAL/NUMERIC column type or string storage, matching the same number-vs-string decision you made in your JSON layer.
5. Practical Checklist
Step 1: Identify Precision-Sensitive Fields
- β Does this field represent money, a large ID, a coordinate, or a scientific measurement?
- β Would losing the last 1-3 digits actually matter for this field?
Step 2: Decide -- Number or String
- β Under ~15 significant digits, always -- keep as a native JSON number
- β Could ever exceed ~15 significant digits -- encode as a JSON string
Step 3: Audit Every Hop in the Pipeline
- β Does any proxy, gateway, or middleware re-parse and re-serialize this JSON?
- β Does the database column type match the precision guarantee you're making?
Step 4: Test With Deliberately Oversized Values
- β Send a value with 18-20 significant digits through the full pipeline end to end
- β Compare what went in against what came out, byte for byte
Step 5: Reject, Don't Silently Round
- β Where possible, validate precision on raw text before parsing
- β Return a clear error instead of accepting and silently rounding a value you can't represent exactly
6. Tools and Debugging
Quick Diagnostic Function
You can catch unsafe numeric literals before you ever call JSON.parse, by scanning the raw text for decimal literals and counting significant digits directly:
function diagnoseNumericPrecision(jsonText) {
const DECIMAL_RE = /-?\d+\.\d+(?:[eE][+-]?\d+)?/g;
const MAX_SAFE_DIGITS = 15;
const matches = jsonText.match(DECIMAL_RE) || [];
console.log('Found', matches.length, 'decimal literal(s)');
matches.forEach((literal) => {
const digitsOnly = literal
.replace(/^-/, '')
.replace(/\./g, '')
.replace(/[eE][+-]?\d+$/, '');
const firstNonZero = digitsOnly.search(/[1-9]/);
const significantDigits = firstNonZero === -1 ? 0 : digitsOnly.length - firstNonZero;
if (significantDigits > MAX_SAFE_DIGITS) {
console.log('β οΈ', literal, '-', significantDigits, 'significant digits (unsafe, will be rounded)');
} else {
console.log('β
', literal, '-', significantDigits, 'significant digits (safe)');
}
});
}
diagnoseNumericPrecision('{"amount": 1234567890.123456789, "count": 42, "lat": 37.774929535}');This is a simplified version of the technique -- a production-grade check also needs to ignore digits that merely appear inside string values (a UUID or a free-text field shouldn't trigger a false positive), which means tracking quote state as you scan rather than running the regex over the raw text unmodified.
Final Thoughts
Numeric precision loss in JSON is easy to miss because it fails the way floating point is supposed to fail -- quietly, by design, with a technically valid result. That's exactly what makes it dangerous in financial, scientific, or identifier-heavy data: nothing in a standard pipeline will tell you it happened.
The fix isn't complicated once you know where to look:
- Identify which fields can't afford to lose digits
- Encode those specific fields as strings instead of numbers
- Check every hop in your pipeline, not just the two endpoints
- Validate on raw text, before parsing -- not after
If you remember one thing from this article: a JSON number that "parses successfully" has told you nothing about whether it parsed correctly. Those are two different claims.
π Want a JSON tool that catches this instead of shipping it silently?
Every processing engine on JSON Pipeline -- JSON Clean, Schema Validator, and AI JSON Repair -- runs a pre-parse precision guard that rejects decimal literals beyond 15 significant digits with a clear error, instead of quietly rounding them like a default JSON.parse would. It currently checks decimal literals specifically; oversized plain integers are on the roadmap. If your data includes financial amounts, coordinates, or other precision-sensitive fields, it's worth running through the full JSON Pipeline toolkit before it reaches production.