Skip to content
All posts

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.

DDDev DeskDeveloper Tools EditorPublished April 27, 20262 min readbeginner

# 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 value
  • NaN — not a JSON value (not in the grammar)
  • Infinity and -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.

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.

New posts, once a week.

Practical developer guides. No spam. Unsubscribe any time.

Tools mentioned

Keep reading