JSON Data Types: String, Number, Boolean, Null, Array & Object

Last updated:

JSON has exactly 6 data types: string (double-quoted UTF-8), number (IEEE 754 double-precision float), boolean (true/false, unquoted), null (unquoted), array (ordered list), and object (unordered key-value pairs) — no integers, dates, undefined, or functions. JSON numbers have no separate integer type — 1 and 1.0 are the same value. JavaScript's Number safely represents integers up to 2^53 − 1 (9,007,199,254,740,991); integers larger than this lose precision when parsed with JSON.parse() — use BigInt or string serialization instead. This guide covers each type's syntax rules and edge cases, the number precision problem (large integers, NaN, Infinity — both forbidden in JSON), null vs undefined (undefined is not JSON), type coercion pitfalls in JSON.parse(), TypeScript type mapping, and JSON5 extensions for additional types.

String: Double Quotes, Unicode Escapes, and Control Characters

A JSON string is a sequence of Unicode characters enclosed in double quotes — single quotes are not valid JSON. Backslash escape sequences handle characters that cannot appear literally: \" (double quote), \\ (backslash), \/ (forward slash, optional), \n (newline), \r (carriage return), \t (tab), \b (backspace), \f (form feed), and \uXXXX (any Unicode code point as four hex digits). Control characters U+0000 through U+001F must be escaped — embedding a raw newline (0x0A) inside a JSON string is invalid. Supplementary characters above U+FFFF must be encoded as a surrogate pair: two \uXXXX sequences.

// Valid JSON strings
"Hello, world!"
"She said \"hello\""          // embedded double quote
"Line 1\nLine 2"              // newline escape
"Tab\there"                  // tab escape
"Unicode: \u00e9"             // é as Unicode escape
"Emoji surrogate: \uD83D\uDE00" // 😀 as surrogate pair (U+1F600)
"Path: C:\\Users\\Alice"     // two backslashes → one literal backslash each

// JSON.parse() examples
JSON.parse('"Hello"')         // "Hello"
JSON.parse('"\u00e9"')       // "é"  — Unicode escape decoded
JSON.parse('"\n"')           // actual newline character (length 1)

// JSON.stringify() escaping
JSON.stringify('Say "hi"')    // '"Say \"hi\""'
JSON.stringify('Line\nTwo')  // '"Line\\nTwo"'  — backslash is escaped
JSON.stringify('\u0000')     // '"\u0000"'  — null byte escaped

// Invalid JSON strings
'single quotes'               // ERROR: must use double quotes
"raw
newline"                      // ERROR: raw newline (U+000A) not allowed
"\uD800"                     // unpaired surrogate — technically invalid per RFC

// String length: JSON has no limit; parsers may impose their own
// Very long strings are valid JSON but may hit memory limits in parsers

The \uXXXX escape accepts exactly four hex digits — é is valid but \ue9 is not. For characters outside the Basic Multilingual Plane (code points above U+FFFF), use surrogate pairs: U+1F600 (😀) encodes as 😀. Modern JSON parsers handle these correctly, but always test emoji handling end-to-end since surrogate pairs can be mangled by encoders that process UTF-16 naively. Notably, the JSON specification permits but does not require escaping forward slashes — \/ and / are both valid inside JSON strings, so parsers must accept both.

Number: IEEE 754, Integer Precision, and Forbidden Values (NaN, Infinity)

JSON numbers are represented as a decimal digit sequence with an optional sign, optional decimal point, and optional exponent — no hex, octal, binary, or leading zeros (except 0.5). RFC 8259 imposes no range or precision limit on JSON numbers, but virtually all parsers store them as IEEE 754 double-precision floats (64-bit), which gives 15-17 significant decimal digits and exact integer representation up to 2^53 − 1. NaN and Infinity are explicitly forbidden by the JSON spec and cannot appear in valid JSON.

// Valid JSON numbers
42
-7
3.14
0.5
1.23e10
-4.5E-3
0
1000000

// Invalid JSON numbers — these are NOT valid JSON
NaN           // ERROR: not allowed in JSON
Infinity      // ERROR: not allowed in JSON
-Infinity     // ERROR: not allowed in JSON
+42           // ERROR: leading + sign not allowed
01            // ERROR: leading zeros not allowed (except "0" itself)
.5            // ERROR: must have digit before decimal point
0xFF          // ERROR: hex not allowed

// IEEE 754 precision boundary
const MAX_SAFE = 9007199254740991  // 2^53 - 1
JSON.parse("9007199254740991")   // 9007199254740991  ✓ exact
JSON.parse("9007199254740992")   // 9007199254740992  ✓ exact (even)
JSON.parse("9007199254740993")   // 9007199254740992  ✗ off by 1 — precision lost!

// NaN and Infinity behavior in JSON.stringify()
JSON.stringify(NaN)              // "null"  — silent conversion!
JSON.stringify(Infinity)         // "null"  — silent conversion!
JSON.stringify(-Infinity)        // "null"  — silent conversion!
JSON.stringify({ x: NaN })       // '{"x":null}'
JSON.stringify([NaN, Infinity])  // '[null,null]'

// Safe large integer pattern: use strings
JSON.stringify({ id: "9999999999999999999" })   // '{"id":"9999999999999999999"}'
BigInt(JSON.parse('"9999999999999999999"'))       // 9999999999999999999n  ✓ exact

// Float precision issues (not JSON-specific — IEEE 754)
JSON.parse("0.1")  // 0.1  (stored as 0.1000000000000000055511...)
0.1 + 0.2          // 0.30000000000000004  — classic float issue

The safest strategy for large integers in JSON is to serialize them as strings on the server and parse them with BigInt() on the client. Many APIs (Twitter/X, Discord, Stripe) already do this — returning numeric IDs both as a number and as a string field (id and id_str) to maintain backward compatibility with JavaScript clients. To detect NaN before serialization, use Number.isNaN(value) — note that the global isNaN() coerces its argument, giving false positives for strings.

Boolean: true and false (Lowercase, Unquoted)

JSON booleans are the literal tokens true and false — always lowercase, never quoted. True, TRUE, "true", 1, and 0 are not JSON booleans. There is no type coercion in the JSON specification: 1 is a number, not a boolean, and a JSON parser must not treat them as equivalent. This is a frequent source of bugs when integrating with languages or databases that represent booleans as integers (PostgreSQL boolean, MySQL TINYINT(1), SQLite — which has no native boolean type).

// Valid JSON booleans
true
false

// Invalid — NOT JSON booleans
True        // ERROR: case-sensitive
TRUE        // ERROR: case-sensitive
"true"      // This is a JSON string, not a boolean
1           // This is a JSON number, not a boolean
0           // This is a JSON number, not a boolean

// JSON.parse() — strict boolean parsing
JSON.parse("true")    // true  (boolean)
JSON.parse("false")   // false (boolean)
JSON.parse('"true"')  // "true" (string — different type!)

// JSON.stringify() — booleans stay booleans
JSON.stringify(true)            // "true"
JSON.stringify(false)           // "false"
JSON.stringify({ ok: true })    // '{"ok":true}'
JSON.stringify({ ok: "true" })  // '{"ok":"true"}'  — note the quotes

// Type coercion pitfalls: truthy/falsy is NOT JSON
// These are JavaScript expressions, not JSON values:
!!0   // false (JS), but 0 is a number in JSON
!!""  // false (JS), but "" is a string in JSON
!![]  // true  (JS), but [] is an array in JSON

// Database integer booleans — must convert before serializing
// MySQL / SQLite often store booleans as 0/1:
const row = { active: 1 }   // from DB
// WRONG: JSON.stringify(row) → '{"active":1}' — number, not boolean
const fixed = { active: row.active === 1 }
// CORRECT: JSON.stringify(fixed) → '{"active":true}'

The case sensitivity of true and false is absolute — any capitalization variant is a JSON syntax error. Databases and ORMs that return integer booleans (0/1) must explicitly convert to JavaScript booleans before serialization: use Boolean(value) or a strict equality check value === 1. TypeScript's strict mode helps catch accidental number-to-boolean confusion at compile time when you declare the type correctly.

null: Meaning, undefined vs null, and JSON.stringify Behavior

JSON null is a first-class type — the literal four-character token null, lowercase and unquoted. It represents the intentional absence of a value: a field that exists in the schema but has no current value. This is semantically different from a missing field (which indicates the key is not part of the data) and from JavaScript's undefined (which is not a JSON concept at all). Understanding this distinction is critical for API design, since null and a missing key communicate different states to consumers.

// null is a valid JSON value anywhere
null                            // valid top-level JSON document
[1, null, 3]                   // valid array with null element
{ "middleName": null }         // valid object with null value

// JSON.parse() with null
JSON.parse("null")             // null  (JavaScript null)
typeof JSON.parse("null")      // "object"  — historical JS quirk

// null vs missing key — different semantics
const withNull    = { "name": "Alice", "middleName": null }
const withMissing = { "name": "Alice" }
// withNull.middleName    → null       (field present, no value)
// withMissing.middleName → undefined  (field absent entirely)
"middleName" in withNull    // true
"middleName" in withMissing // false

// undefined: what JSON.stringify() does with it
JSON.stringify(undefined)                // undefined (not a string — returns JS undefined)
JSON.stringify({ a: undefined })         // '{}'             — key silently dropped!
JSON.stringify({ a: undefined, b: 1 })  // '{"b":1}'        — a is gone
JSON.stringify([undefined, 1])           // '[null,1]'       — undefined → null in arrays
JSON.stringify([undefined])             // '[null]'

// Round-trip loss: undefined is not preserved
const original = { a: undefined, b: 1 }
const roundTrip = JSON.parse(JSON.stringify(original))
// roundTrip → { b: 1 }  — property "a" is completely gone!

// JSON.stringify replacer: convert undefined to null explicitly
JSON.stringify(
  { a: undefined, b: 1 },
  (key, value) => (value === undefined ? null : value)
)
// '{"a":null,"b":1}'  — now a is preserved as null

// null coalescing vs optional chaining
const data = JSON.parse('{"user": null}')
data.user?.name    // undefined (optional chain handles null)
data.user ?? "anon"  // "anon"   (nullish coalescing)

The silent dropping of undefined properties by JSON.stringify() is one of the most common sources of data loss in JavaScript APIs. If your schema requires a field to always be present (even without a value), always serialize it as null rather than leaving it as undefined. Use a replacer function or explicit null assignment to enforce this. In TypeScript, model nullable fields as T | null rather than T | undefined when the field must survive JSON serialization.

Array: Ordered Lists, Nested Types, and Empty Arrays

A JSON array is an ordered sequence of zero or more JSON values, enclosed in square brackets and separated by commas. Array elements can be any mix of JSON types — there is no requirement that all elements share the same type. Arrays preserve insertion order, which is guaranteed by the JSON specification. Empty arrays ([]) are valid. Trailing commas after the last element are not valid JSON (they are allowed in JSON5 and JavaScript object literals but rejected by JSON.parse()).

// Valid JSON arrays
[]                             // empty array
[1, 2, 3]                     // numbers
["a", "b", "c"]               // strings
[true, false, null]           // mixed primitive types
[1, "two", true, null, [3]]  // mixed types including nested array

// Nested arrays and objects
[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

// Deeply nested arrays — valid but watch parser depth limits
[[[[1, 2], [3, 4]], [[5, 6]]], [[[7]]]]

// Invalid JSON arrays
[1, 2, 3,]      // ERROR: trailing comma not allowed
[1,,3]          // ERROR: elided elements not allowed (unlike JS arrays)
[1 2 3]         // ERROR: commas required between elements

// JSON.parse() — array access
const arr = JSON.parse('[10, "hello", true, null, [1,2]]')
arr[0]          // 10        (number)
arr[1]          // "hello"   (string)
arr[2]          // true      (boolean)
arr[3]          // null
arr[4]          // [1, 2]    (nested array)
arr[4][0]       // 1

// Array of objects — common API response pattern
const users = JSON.parse('[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]')
users.map(u => u.name)  // ["Alice", "Bob"]

// JSON.stringify() array behavior
JSON.stringify([undefined, 1, null])   // '[null,1,null]'  — undefined → null
JSON.stringify([() => {}, Symbol()])   // '[null,null]'    — functions/Symbols → null
JSON.stringify([])                     // '[]'

// Preserve order — arrays are ordered by spec
const original = [3, 1, 2]
JSON.parse(JSON.stringify(original))   // [3, 1, 2]  — order preserved

JSON arrays do not support sparse elements — there is no equivalent of JavaScript's [1,,3] (which creates an array with a hole at index 1). Every position in a JSON array must have an explicit value; use null to represent an intentionally empty slot. When JSON.stringify() encounters functions or Symbol values inside arrays, it converts them to nullrather than omitting the element, preserving the array's length and element positions — a different behavior from the property-omitting behavior for objects.

Object: Key Rules, Duplicate Keys, and Ordering

A JSON object is an unordered collection of key-value pairs enclosed in curly braces. Keys must be strings (double-quoted) — unlike JavaScript object literals, bare identifiers are not allowed. Values can be any JSON type. The JSON specification says key order is not guaranteed, though most parsers preserve insertion order in practice (V8/Node.js does, as do Python 3.7+ and most others). Duplicate keys are technically permitted by RFC 8259 but behavior is undefined — most parsers keep the last value for a duplicate key.

// Valid JSON objects
{}                                          // empty object
{ "name": "Alice" }                        // single key
{ "id": 1, "name": "Alice", "active": true }  // multiple keys

// All key types must be strings (double-quoted)
{ "key": "value" }    // valid
{ "123": "value" }    // valid — numeric string key
{ "": "value" }       // valid — empty string key

// Invalid JSON objects
{ key: "value" }      // ERROR: key must be quoted
{ 'key': "value" }    // ERROR: single-quoted key not allowed
{ "key": "value", }   // ERROR: trailing comma

// Nested objects
{
  "user": {
    "id": 1,
    "address": {
      "city": "New York",
      "zip": "10001"
    }
  }
}

// Duplicate keys — behavior is undefined per spec
JSON.parse('{"a":1,"a":2}')   // {a: 2}  — last value wins in V8/Node.js
JSON.parse('{"a":2,"a":1}')   // {a: 1}  — last value wins

// Key ordering — not guaranteed by JSON spec
// Most modern parsers preserve insertion order in practice:
const obj = JSON.parse('{"z":1,"a":2,"m":3}')
Object.keys(obj)  // ["z", "a", "m"]  — insertion order preserved in V8

// JSON.stringify() with objects
JSON.stringify({ a: undefined, b: 1 })   // '{"b":1}'  — undefined dropped
JSON.stringify({ fn: () => {}, b: 1 })   // '{"b":1}'  — function dropped
JSON.stringify({ sym: Symbol(), b: 1 })  // '{"b":1}'  — Symbol dropped

// Controlling key order with JSON.stringify replacer array
JSON.stringify({ z: 1, a: 2, m: 3 }, ["a", "m", "z"])
// '{"a":2,"m":3,"z":1}'  — keys in replacer order

// toJSON() method — custom serialization for objects
const obj2 = {
  secret: "hidden",
  name: "Alice",
  toJSON() { return { name: this.name } }
}
JSON.stringify(obj2)  // '{"name":"Alice"}'  — toJSON controls output

The toJSON() method on an object is called by JSON.stringify() when present — it is the JavaScript equivalent of PHP's JsonSerializable. Date objects use toJSON() to serialize as ISO 8601 strings. Passing an array as the second argument to JSON.stringify() acts as an allowlist and controls output key order — a useful trick for generating canonical JSON for comparison or hashing, since JSON object key order is otherwise implementation-defined.

TypeScript Type Mapping for JSON Types

TypeScript has no built-in JSON type, but all 6 JSON types map cleanly to TypeScript primitives and structures. The standard pattern is a recursive JsonValue type alias that covers all valid JSON values. For known data shapes, prefer typed interfaces over JsonValue to get compile-time checking. After JSON.parse(), which returns any, narrow the type immediately using a type assertion (as MyType) or a runtime validator such as Zod.

// JSON type → TypeScript type mapping
// JSON string  → string
// JSON number  → number
// JSON boolean → boolean
// JSON null    → null
// JSON array   → T[] or JsonValue[]
// JSON object  → Record<string, JsonValue> or a typed interface

// Recursive JsonValue type — covers all valid JSON
type JsonPrimitive = string | number | boolean | null
type JsonArray     = JsonValue[]
type JsonObject    = { [key: string]: JsonValue }
type JsonValue     = JsonPrimitive | JsonArray | JsonObject

// Typed interface for known shapes
interface User {
  id: number
  name: string
  email: string | null   // null means field present but no value
  tags: string[]
  address?: {            // optional = may be absent entirely
    city: string
    zip: string
  }
}

// JSON.parse() returns any — narrow immediately
const raw: unknown = JSON.parse(jsonString)  // prefer unknown over any
const user = raw as User  // type assertion — unchecked at runtime!

// Safe runtime validation with Zod (recommended)
import { z } from "zod"

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().nullable(),    // string | null
  tags: z.array(z.string()),
  address: z.object({
    city: z.string(),
    zip: z.string(),
  }).optional(),
})

type User = z.infer<typeof UserSchema>  // derive type from schema

const user2 = UserSchema.parse(JSON.parse(jsonString))
// Throws ZodError if JSON does not match schema
// Returns typed User if valid

// Null vs undefined in TypeScript JSON types
interface ApiResponse {
  name: string
  middleName: string | null   // CORRECT: present in JSON but may be null
  nickname?: string            // WRONG for JSON: may be dropped by JSON.stringify!
}

// Generic JSON fetch helper with type parameter
async function fetchJson<T>(url: string, schema: z.ZodType<T>): Promise<T> {
  const res = await fetch(url)
  const raw: unknown = await res.json()
  return schema.parse(raw)  // throws if invalid
}

// Usage:
const users = await fetchJson("/api/users", z.array(UserSchema))

The critical distinction in TypeScript JSON typing is T | null vs optional (T | undefined / ?): nullable fields (string | null) survive JSON serialization intact, while optional fields may be silently dropped if their value is undefined. Use null for JSON fields that must always be present in the serialized output even when they have no value. For runtime validation, Zod's .nullable() maps to T | null and .optional() maps to T | undefined — use the correct one based on whether the JSON field is always present. See the JSON.parse() in JavaScript and TypeScript JSON types guides for deeper coverage.

Key Terms

JSON string type
One of the 6 JSON data types: a sequence of zero or more Unicode characters enclosed in double quotes. Backslash escape sequences are required for control characters (U+0000–U+001F), double quotes (\"), and backslashes (\\). Unicode code points can be expressed as \uXXXX (four hex digits); supplementary characters above U+FFFF use surrogate pairs (two \uXXXX sequences). Single quotes are not valid delimiters in JSON strings. There is no length limit in the JSON specification, though parsers may enforce their own limits. The empty string "" is a valid JSON string. JSON strings map to string in TypeScript, str in Python, and String in Java.
IEEE 754 double precision
The 64-bit floating-point format used by virtually all JSON parsers to represent JSON numbers. Defined by IEEE 754-2008, it stores numbers as a sign bit, 11 exponent bits, and 52 mantissa bits, providing approximately 15–17 significant decimal digits. It can represent integers exactly up to 2^53 − 1 (9,007,199,254,740,991) — beyond this, consecutive integers are no longer representable, causing precision loss. Special values NaN, +Infinity, and -Infinity exist in IEEE 754 but are forbidden in JSON. JavaScript's Number type is an IEEE 754 double; this is why Number.MAX_SAFE_INTEGER is 9,007,199,254,740,991 and why large integer IDs require special handling in JavaScript JSON code.
JSON null
One of the 6 JSON data types: the literal four-character token null, always lowercase and unquoted. Represents the intentional absence of a value — a field is present in the data structure but has no value. Semantically distinct from a missing key (which means the field is not part of the data at all) and from JavaScript's undefined (which is not a JSON concept). JSON null maps to null in JavaScript and TypeScript, None in Python, null in Java, and NULL in SQL. JSON.parse("null") returns JavaScript null. JSON.stringify(null) returns the string "null".
JSON number precision
The practical constraint that JSON numbers are limited to IEEE 754 double-precision range and precision in most parsers, even though the JSON specification itself imposes no limit. Integers are represented exactly up to 2^53 − 1; beyond this limit, integers are rounded to the nearest representable double. The precision problem manifests silently — JSON.parse("9007199254740993") returns 9007199254740992 with no error or warning. Mitigation strategies: serialize large integers as JSON strings ("9007199254740993"), use a specialized parser with BigInt support, or use the JSON_BIGINT_AS_STRING flag in PHP. APIs dealing with 64-bit IDs (database primary keys, distributed system IDs, Twitter snowflakes) must handle this to avoid data corruption.
JSON object key ordering
RFC 8259 explicitly states that JSON objects are unordered collections and that parsers should not rely on key order. In practice, most modern parsers preserve insertion order: V8 (Node.js, Chrome), Python 3.7+, and most Java parsers maintain the order keys appear in the JSON string. However, this behavior is implementation-specific and not guaranteed by the specification. Code that depends on a specific key order for correctness (such as canonical JSON for signing or hashing) must sort keys explicitly. JSON.stringify() in JavaScript iterates object properties in the order they appear in the object, but this order can be influenced by the property creation order and integer key sorting. Use the replacer array argument to control output key order deterministically.
JSON5
A superset of JSON that extends the syntax to support additional types and more human-friendly formatting, based on ECMAScript 5.1. JSON5 additions include: single-quoted strings, unquoted object keys (bare identifiers), trailing commas in objects and arrays, multi-line strings (backslash before newline), hexadecimal numbers (0xFF), explicit positive signs (+1), Infinity, -Infinity, and NaN as number values, and single-line (//) and multi-line (/* */) comments. JSON5 is useful for configuration files where human readability matters (e.g., tsconfig.json and VS Code settings.json use a JSON5-like superset). JSON5 is not compatible with standard JSON parsers — use the json5 npm package to parse it. Standard JSON parsers will reject JSON5 syntax.

FAQ

What are the JSON data types?

JSON has exactly 6 data types: string (a sequence of Unicode characters in double quotes), number (an IEEE 754 double-precision float — no separate integer type), boolean (the literals true or false, lowercase and unquoted), null (the literal null, lowercase and unquoted), array (an ordered list of JSON values in square brackets), and object (a collection of string-keyed JSON values in curly braces). There is no integer type, no date type, no undefined type, no binary type, and no function type. Any value not representable by these 6 types must be encoded as a string or excluded from the JSON entirely.

Does JSON have an integer type?

No. JSON has a single number type with no integer/float distinction — 1 and 1.0 are the same JSON value. The JSON specification (RFC 8259) defines numbers as a digit sequence with optional sign, optional decimal, and optional exponent. Most parsers return different language types depending on the value: JavaScript's JSON.parse() always returns a Number; Python's json.loads() returns int for 42 and float for 42.0; Java's Jackson picks int, long, or double by value range. This language-level distinction is a parser implementation detail, not part of the JSON type system.

How does JSON handle large numbers?

The JSON spec places no limit on number size, but most parsers use IEEE 754 doubles, which lose precision for integers above 2^53 − 1 (9,007,199,254,740,991). JSON.parse("9007199254740993") silently returns 9007199254740992 in JavaScript — one less than the correct value, with no error. The standard solution is to serialize large integers as JSON strings: {"id":"9999999999999999999"}. Parse them back with BigInt(value). Major APIs (Twitter/X, Discord) use this pattern, often providing both a numeric field and a string field for the same ID. Python's json module handles large integers correctly since Python int has arbitrary precision.

Is null valid in JSON?

Yes. null is one of the 6 JSON types and is valid anywhere a JSON value is expected — as a top-level document (JSON.parse("null") === null), inside arrays ([1, null, 3]), or as an object value ({"key": null}). It must be written as the exact four-character lowercase token null. Null, NULL, and "null" (a string) are different values. JSON null signals that a field intentionally has no value, as opposed to a missing key, which signals the field is not present in the data at all.

What is the difference between null and undefined in JSON?

JSON has null but does not have undefinedundefined is a JavaScript-only concept. JSON.stringify() silently drops object properties with undefined values: JSON.stringify({ a: undefined }) returns "{}", not '{"a":null}' — the key disappears entirely. In arrays, undefined is converted to null rather than removed. JSON.parse() never produces undefined. The practical consequence: if you need a field to survive a JSON round-trip even when it has no value, set it to null — never leave it undefined. In TypeScript, model nullable JSON fields as T | null, not T | undefined.

Why does JSON.stringify(NaN) return null?

NaN and Infinity are valid IEEE 754 float values but are explicitly forbidden by the JSON specification (RFC 8259). Since they cannot be represented as valid JSON numbers, JSON.stringify() converts them to null rather than throwing an error — a silent, lossy conversion. JSON.stringify(NaN) returns "null"; JSON.stringify(Infinity) returns "null"; JSON.stringify({ x: NaN }) returns '{"x":null}'. To detect this before it happens, check Number.isNaN(value) or !isFinite(value) before serializing. Consider throwing an error or substituting a sentinel value (0, null, or a string like "NaN") explicitly rather than accepting the silent conversion.

Can JSON object keys be duplicated?

RFC 8259 says JSON object keys SHOULD be unique but does not make duplicates a syntax error. Behavior with duplicate keys is defined as undefined by the spec — parsers may keep the first value, the last value, all values, or throw an error. In practice, most parsers keep the last value: JSON.parse('{"a":1,"a":2}') returns { a: 2 } in JavaScript and Python. Some strict parsers or JSON linters throw an error on duplicate keys. From a producer standpoint, always generate JSON with unique keys — duplicate keys are a schema error that can cause subtle data loss depending on which parser the consumer uses. To accumulate multiple values for the same logical key, use a JSON array value instead.

How do I type JSON in TypeScript?

Define a recursive JsonValue type: type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }. For known shapes, use a typed interface: interface User { id: number; name: string; email: string | null; }. After JSON.parse() — which returns any — narrow the type immediately. For production code, use a runtime validator: Zod (z.object({...}).parse(JSON.parse(json))) validates the actual data at runtime and returns a typed result, catching mismatches that TypeScript's as assertion silently misses. Model nullable fields as T | null (not T | undefined) so they survive JSON.stringify() without being dropped. See the TypeScript JSON types guide for advanced patterns.

Further reading and primary sources