Skip to content
academia.sh

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 nullundefined 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.stringify converts a value to text, JSON.parse converts 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 to null. NaN and Infinity become null in 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.

Start typing to search.

↑↓ Esc navigate · open · close