NaN, null and undefined in JSON: what serialises to what
JSON has null. JavaScript has null, undefined and NaN. Python has None. This is the table of what survives each conversion — and the traps everyone hits.
# The values, and what JSON can represent
JSON can represent five scalar types: null, true, false, strings, and numbers. Notably missing:
undefined— not a JSON valueNaN— not a JSON value (not in the grammar)Infinityand-Infinity— not JSON values- Date objects — not JSON values (serialise as strings, usually ISO 8601)
BigInt— not JSON values
Every language has to map its richer type system onto JSON's five scalars, and they disagree about how.
# JavaScript
| Value | JSON.stringify output | Round-trip result |
|------------------|-------------------------|---------------------------|
| null | null | null |
| undefined | omitted (object) / null (array) | missing (object) / null (array) |
| NaN | null | null |
| Infinity | null | null |
| new Date(...) | ISO 8601 string | string (not a Date!) |
| 123n (BigInt) | throws | — |
<div class="callout callout-warning" role="note"><div class="callout-title">Warning</div><div class="callout-body"><p><code>JSON.stringify({ x: NaN })</code> → <code>'{"x":null}'</code> — silently. If you're storing floats and any of them could be NaN, you lose the difference between NaN and null on round-trip.</p></div></div>
JSON.stringify({ a: undefined, b: null, c: NaN, d: new Date() });
// '{"b":null,"c":null,"d":"2026-04-27T10:30:00.000Z"}'
// Note: 'a' is gone entirely, 'c' became null, 'd' is a string now
JSON.stringify([undefined, NaN, Infinity]);
// '[null,null,null]'
// In arrays, undefined becomes null (can't leave gaps)
# Python
Python's json module is stricter than JavaScript but has a permissive mode.
| Value | json.dumps output | Round-trip result |
|--------------------|------------------------|---------------------------|
| None | null | None |
| float("nan") | NaN (invalid JSON!) | float("nan") |
| float("inf") | Infinity (invalid!) | float("inf") |
| datetime(...) | TypeError | — |
| Decimal("1.23") | TypeError | — |
Python's default violates the spec:
import json
json.dumps({"x": float("nan")})
# '{"x": NaN}' ← not valid JSON!
For strict JSON, use allow_nan=False:
json.dumps({"x": float("nan")}, allow_nan=False)
# raises ValueError
And handle dates explicitly:
import json
from datetime import datetime
def default(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError
json.dumps({"when": datetime.now()}, default=default)
# '{"when": "2026-04-27T10:30:00"}'
# Go
Strict by default — the encoder refuses to emit invalid JSON.
| Value | json.Marshal output | Round-trip result |
|--------------------|------------------------|---------------------------|
| nil | null | nil (if pointer) |
| math.NaN() | error | — |
| math.Inf(1) | error | — |
| time.Time{} | RFC 3339 string | time.Time via UnmarshalJSON |
| empty struct field | included | included |
Go nils in struct fields encode as null unless you use omitempty:
type User struct {
Name string `json:"name"`
Age *int `json:"age,omitempty"`
}
// Age = nil → omitted entirely
// Age = &v → included as number
<div class="callout callout-tip" role="note"><div class="callout-title">Tip</div><div class="callout-body"><p>Use pointer types + <code>omitempty</code> when you need to distinguish "not set" from "zero value". <code>Age int</code> can't tell 0 from unset; <code>Age *int</code> can.</p></div></div>
# Cross-language round-trip table
What survives when you serialise in language A, deserialise in language B?
| A → B | null | NaN | Infinity | undefined | date |
|-----------------|------|-----|----------|-----------|--------------|
| JS → JS | ✓ | → null | → null | → missing/null | string |
| JS → Python | ✓ | → null | → null | → missing | string |
| Python → JS | ✓ | → NaN| → Infinity| N/A | TypeError unless default= |
| Python → Python | ✓ | ✓ | ✓ | N/A | TypeError unless default= |
| Go → anywhere | ✓ | error | error | N/A | string |
*Python emits non-standard NaN/Infinity; strict JS parsers (like JSON.parse) reject them.
# The rules that make cross-language work
1. Don't serialise NaN or Infinity. Convert them to null, a sentinel string ("NaN"), or a separate boolean field (isNaN: true).
2. Dates as ISO 8601 strings. See our ISO 8601 cheatsheet. Everyone can parse those.
3. BigInts as strings. JSON numbers are floats. If you need >53-bit precision, send the number as a quoted string.
4. Be explicit about null vs missing. Especially in PATCH APIs — document which means "don't change this field" and which means "set this to null".
5. Validate on parse, not on emit. A strict JSON.parse + schema check (Zod, Yup, Pydantic) catches cross-language incompatibilities at the right layer.
# Try it
Paste anything into our JSON Formatter & Validator to see what's valid and what isn't. Invalid tokens (NaN, undefined, trailing commas) are highlighted with line + column.
# Related reading
Frequently asked questions
›Why isn't NaN valid JSON?
The JSON spec only defines `null`, `true`, `false`, numbers, strings, arrays and objects. `NaN` and `Infinity` aren't in the grammar. Strict parsers reject them; permissive ones accept them as an extension.
›Should I send `null` or omit the field?
Depends on your API contract. `null` means 'explicitly no value'; omitted means 'the field isn't set'. They're semantically different for PATCH-style APIs. REST conventions favour explicit `null`; some GraphQL styles favour omission.