JSON Lint / Edge cases

NaN and Infinity in JSON

Strict JSON has no way to represent NaN, +Infinity, or -Infinity. Different languages handle the absence differently — some silently substitute null, some emit the literal token NaN anyway, some throw. The bugs this causes are usually invisible until the data hits a strict consumer.

Validate your JSON now →

What the spec says

RFC 8259 §6 defines the JSON number grammar:

number = [ minus ] int [ frac ] [ exp ]

NaN, Infinity, and -Infinity do not match this grammar. Strict parsers reject them. The spec also notes that JSON cannot represent the full range of IEEE 754, which is why "valid JSON number" and "valid IEEE 754 double" are not the same set.

What each language actually emits

LanguageEncodes NaN asEncodes Infinity asStrict mode?
JavaScript (JSON.stringify)nullnulln/a — silent
Python (json.dumps)NaN (invalid)Infinity (invalid)allow_nan=False raises
Go (encoding/json)Returns UnsupportedValueErrorSameDefault; cannot disable
Rust (serde_json)Returns errorSameDefault
Java (Jackson)NaN (invalid by default)SameQUOTE_NON_NUMERIC_NUMBERS emits "NaN"
pandas (to_json)nullnulln/a — silent
NumPy (via json)Inherits Python's behaviorSameSame

Two distinct failure modes:

The pandas → browser pipeline (most common bug)

A backend computes a DataFrame, calls df.to_json(), and ships it to a frontend. NaN values become null silently. The frontend renders null as "—" or 0 or skips the row, and the discrepancy with the source Excel file is reported as a frontend bug. It isn't.

Fix: explicitly decide on the backend what missing means. Replace NaN with a documented sentinel string ("missing") or carry a separate "is_present" boolean alongside the number. Don't let to_json's default decide.

Workarounds

1. Replace before serializing

// JS
const safe = JSON.stringify(value, (k, v) =>
  typeof v === "number" && !Number.isFinite(v) ? null : v
);

# Python
import math
def clean(o):
    if isinstance(o, float) and not math.isfinite(o): return None
    if isinstance(o, dict): return {k: clean(v) for k, v in o.items()}
    if isinstance(o, list): return [clean(x) for x in o]
    return o
json.dumps(clean(value), allow_nan=False)

2. Encode as strings

If the value matters, send it as a string consumers know how to decode:

{"latency_ms": "Infinity"}

Lossless, but every consumer needs to opt in.

3. Use JSON5 or NDJSON with extensions

JSON5 allows NaN and Infinity directly. Some streaming dialects (e.g., extended JSON for MongoDB) define {"$numberDouble": "NaN"} as the canonical encoding. Either is fine if you control both ends.

Detection: is your JSON about to break someone?

Run the input through any strict parser other than the one that produced it. If your producer is Python json with defaults and your consumer is a browser, paste the output into our linter — it uses the browser's strict parser and will flag the bare NaN token.

Related

FAQ

Why does JSON forbid NaN and Infinity?

RFC 8259 defines a JSON number as a decimal literal: optional minus, integer part, optional fraction, optional exponent. NaN and Infinity aren't representable in that grammar. The original spec authors prioritized portability across languages that don't all have IEEE 754 specials.

What does JSON.stringify do with NaN?

It silently outputs 'null'. Same for +Infinity, -Infinity, and undefined. No error, no warning. This is the most common cause of mysterious nulls landing in databases.

What does Python's json.dumps do with NaN?

By default, json.dumps emits the literal string 'NaN' (without quotes), which is valid Python but not valid JSON — and json.loads accepts it back, which is what makes the bug feel local. Pass allow_nan=False to force a ValueError. Standards-compliant consumers (browsers, Go, Rust) will reject the output.

What about JSON5?

JSON5 explicitly allows NaN, Infinity, and -Infinity as numeric literals. If your producer and consumer both use JSON5, you can keep them. If either side is strict JSON, you can't.

Should I use null, a string, or a sentinel number?

Depends on what 'missing' means. If the field is genuinely unknown, use null and document it. If it's the result of 0/0, store the operation outcome separately ('status': 'undefined') so consumers don't confuse it with a real number. Sentinel numbers (-1, 9999) are a trap: they look like data and silently corrupt aggregations.

Open the linter →