A value JSON.stringify can serialize.
Recursive: an array's elements and an object's property values are
themselves JsonValues, so a nested payload is checked all the way down
rather than only at its top level.
The object arm admits undefined deliberately. JSON.stringify drops a
property whose value is undefined rather than failing, so
{ note: string | undefined } — the shape an optional field takes once it is
written out — is serializable and is accepted here. What the type rejects is
the set JSON.stringify cannot represent: bigint (which throws), plus
functions and symbols (which it silently drops, losing data the caller
believed it was sending).
Three limits are worth knowing before you reach them, and none is
expressible in a type. A circular structure throws at runtime; no type can
express acyclicity. A named interface is not assignable, because TypeScript
grants implicit index signatures only to object-literal types — declare the
payload with a type alias, or extend Record<string, JsonValue | undefined>. And NaN, Infinity and -Infinity are members of number
that JSON has no representation for: JSON.stringify normalizes each of them
to null rather than failing, so the value is silently changed rather than
refused. TypeScript cannot exclude them from number, and a runtime check
would mean walking every payload on the hot path — send the number as a
string when the distinction matters.