Lesson 20 / 21
JSON
Serialization and parsing, values the format does not support, circular references, callbacks that adapt the conversion, and the limit of number precision.
Contents
The previous lesson produced readable report text. That text was for humans; it is not a format the machine can read back. This lesson takes up JSON: converting data to text and reading it back.
JSON is a data format derived from JavaScript syntax but independent of it. This origin makes the two look close to each other while not fully overlapping. Knowing the distinction prevents silent data loss.
Serialization and Parsing
const record = { station: "A1", temperature: 21.5, humidity: null, validated: false }; const text = JSON.stringify(record); console.log(text); console.log(typeof text); console.log(JSON.stringify(record, null, 2)); const back = JSON.parse(text); console.log(back, back.temperature + 1); console.log(record === back);
{"station":"A1","temperature":21.5,"humidity":null,"validated":false}
string
{
"station": "A1",
"temperature": 21.5,
"humidity": null,
"validated": false
}
{ station: 'A1', temperature: 21.5, humidity: null, validated: false } 22.5
false
JSON.stringify converts a value to text; the third argument is the indent amount and
produces readable output. JSON.parse produces a new value from text.
The last line matters: the result of the conversion is a new object. By the identity rule from the Equality Comparisons lesson, it is not equal to the original object. This is also a way of taking a deep copy — but only for values the format supports.
The third block in the output is JSON text: keys are double-quoted. The object display at the end is the runtime’s inspection format; the unquoted keys and single-quoted text come from there. The two are different things.
Values the Format Does Not Support
JSON’s set of values is narrower than the language’s set of values. Unsupported values are handled silently:
const record = { station: "A1", missing: undefined, fn: () => 1, symbol: Symbol("x"), invalid: NaN, infinite: Infinity, }; console.log(JSON.stringify(record)); console.log(JSON.stringify([undefined, () => 1, NaN]));
{"station":"A1","invalid":null,"infinite":null}
[null,null,null]
Two different behaviors are at play. On object properties, undefined, function, and
symbol values are skipped entirely: the key never shows up in the output. On array
elements, the same values are converted to null, because the array’s length has to be
preserved.
NaN and Infinity become null in both cases. This is because the format does not
recognize these values at all. The result is silent: an invalid measurement turns, once
parsed, into null — which means “deliberately empty.” The null–undefined
distinction established in the Primitive Types lesson does not survive the conversion.
In two cases, an error is thrown:
const a = { station: "A1" }; a.itself = a; try { JSON.stringify(a); } catch (error) { console.log(error.name); } try { JSON.stringify({ counter: 1n }); } catch (error) { console.log(error.name + ": " + error.message); }
TypeError TypeError: Do not know how to serialize a BigInt
A circular reference — an object directly or indirectly containing itself — would produce an infinite text. Big integers are rejected because, as discussed in the BigInt lesson, the format has no precision guarantee for them. The first error’s message varies by runtime, so only its name was printed.
Adapting the Conversion
Both methods can take a callback. In serialization, the callback is called for every
key–value pair, and the value it returns gets written; returning undefined skips the
field.
const record = { station: "A1", timeNs: 1700000000123456789n, temperature: 21.5, secret: "do not write", }; const text = JSON.stringify(record, (key, value) => { if (key === "secret") return undefined; if (typeof value === "bigint") return value.toString(); return value; }); console.log(text); const back = JSON.parse(text, (key, value) => key === "timeNs" ? BigInt(value) : value, ); console.log(back.timeNs === record.timeNs, typeof back.timeNs);
{"station":"A1","timeNs":"1700000000123456789","temperature":21.5}
true bigint
The problem left open in the BigInt lesson is finished here: the value was written as
text, converted back on reading, and preserved down to its last digit. The secret
field did not enter the output.
The two callbacks are symmetric and should be designed together: whichever field the writing side converted to which format, the reading side has to convert that same field back. Otherwise the type changes silently.
Instead of a callback, serialization can also be given a list of fields:
const record = { station: "A1", temperature: 21.5, humidity: 48 }; console.log(JSON.stringify(record, ["station", "temperature"]));
{"station":"A1","temperature":21.5}
The Limit of Number Precision
Numbers in JSON text are converted to the language’s number type when parsed. The safe integer range from the Primitive Types lesson applies here too:
console.log(JSON.parse("9007199254740993")); console.log(JSON.parse("0.1") + JSON.parse("0.2")); console.log(JSON.stringify(0.1 + 0.2));
9007199254740992 0.30000000000000004 0.30000000000000004
The first line is a silent loss: an integer correctly written in the text landed on a different value once read. The format could carry this value; what loses it is not the parser, it is the target type. For lossless transfer, the value is written as text — this is the callback pattern above.
The last two lines show floating-point representation surviving the conversion: the written text returns to the same bit pattern.
Parsing Is Strict
JSON’s grammar is narrower than JavaScript’s object syntax:
const broken = ['{"a":1,}', "{'a':1}", "{a:1}", ""]; for (const m of broken) { try { JSON.parse(m); console.log(JSON.stringify(m), "-> valid"); } catch (error) { console.log(JSON.stringify(m), "->", error.name); } }
"{\"a\":1,}" -> SyntaxError
"{'a':1}" -> SyntaxError
"{a:1}" -> SyntaxError
"" -> SyntaxError
All four are valid or harmless writings in JavaScript source; not in JSON. A trailing comma, single quotes, an unquoted key, and empty text are all rejected. This strictness is what lets the format be read the same way across different languages.
The error name is SyntaxError; the message text varies by parser. The rule from the
Error Handling lesson applies here: branching is done on the name.
Applying It to the Measurement Script
The course’s thread can now write a full round trip: read from text, transform, write back to text.
const incoming = `[ {"station":"A1","temperature":21.5,"humidity":48}, {"station":"A2","temperature":19.75,"humidity":null}, {"station":"B1","temperature":23,"humidity":41} ]`; const records = JSON.parse(incoming); const enriched = records.map((r) => ({ ...r, deviation: Number((r.temperature - 20).toFixed(2)), hasHumidity: r.humidity != null, })); console.log(JSON.stringify(enriched, null, 2));
[
{
"station": "A1",
"temperature": 21.5,
"humidity": 48,
"deviation": 1.5,
"hasHumidity": true
},
{
"station": "A2",
"temperature": 19.75,
"humidity": null,
"deviation": -0.25,
"hasHumidity": false
},
{
"station": "B1",
"temperature": 23,
"humidity": 41,
"deviation": 3,
"hasHumidity": true
}
]
Three decisions were made together. The transformation produced new objects; by the
rule from the Objects lesson, the input was not changed. The deviation value was
rounded with toFixed and converted back to a number with Number — otherwise it
would have been written to JSON as text. The humidity field’s presence was tested
with a != null check, so a null value was also counted as missing.
If the text being parsed comes from outside, parsing should be wrapped in a
try/catch; broken input should not stop the program.
Summary
JSON.stringifyconverts a value to text,JSON.parseconverts text to a new value; the result of the conversion is not equal to the original object.- On object properties,
undefined, function, and symbol are skipped; on array elements, they are converted tonull.NaNandInfinitybecomenullin every case. - Circular references and big integers throw during serialization.
- Callbacks make lossless transfer of unsupported types possible; the writing and reading sides are written symmetrically.
- JSON numbers are converted to the language’s number type; integers above the safe range are silently rounded.
- JSON’s grammar is strict: a trailing comma, single quotes, and an unquoted key are all rejected.
Next Step
The measurement records are still timeless: it is not known when any given reading was taken. The next lesson takes up date and time representation — the timestamp concept, the timezone/local-time distinction, parsing traps, and formatting. The course’s closing will happen there too.
To keep your progress and take notes, Log in
My notes
Log in to take notes.