JavaScript Basics: Parse, Validate, and Write JSON
JSON is text for exchanging data. A JavaScript object is a value your program can use. Crossing that boundary takes two familiar functions: JSON.parse() reads JSON text, and JSON.stringify() produces it. A third step belongs to your application: checking whether the data has the fields and types you expect.
We will build a small settings reader, handle rejected input, and produce an explicit export. Each example is independent, uses in-memory data, and makes no network requests.
Prepare a scratch directory
These examples were tested on Linux with Node.js v24.19.0. You need Node.js on your PATH, Bash, the standard mktemp, rm, and rmdir commands, and a text editor. There are no external dependencies, installation commands, environment variables, or input files to configure. The .mjs extension enables JavaScript modules without a package file. Assertions use Node’s built-in node:assert/strict module.
Run this in Bash and keep the same shell open through cleanup:
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested version command printed:
v24.19.0Use your editor to save each named file below in this directory with UTF-8 encoding. Copy only its JavaScript block into the file, then run the separate Bash command.
Parse text, then validate its shape
Save as read-json-settings.mjs:
import assert from 'node:assert/strict';
function readSettings(text) {
const value = JSON.parse(text);
if (value === null || typeof value !== 'object' ||
Array.isArray(value)) {
throw new TypeError('Settings must be an object');
}
if (typeof value.theme !== 'string' ||
!['light', 'dark'].includes(value.theme)) {
throw new TypeError('theme must be light or dark');
}
if (!Number.isInteger(value.pageSize) ||
value.pageSize < 1 || value.pageSize > 100) {
throw new TypeError('pageSize must be an integer from 1 to 100');
}
return { theme: value.theme, pageSize: value.pageSize };
}
const settings = readSettings('{"theme":"dark","pageSize":20}');
assert.deepEqual(settings, { theme: 'dark', pageSize: 20 });
assert.throws(() => readSettings('null'), TypeError);
assert.throws(() => readSettings('[]'), TypeError);
assert.throws(() => readSettings('{"theme":"dark","pageSize":"20"}'), TypeError);
assert.throws(() => readSettings('{"theme":"dark","pageSize":0}'), TypeError);
console.log(`Theme: ${settings.theme}; page size: ${settings.pageSize}`);node read-json-settings.mjsOutput:
Theme: dark; page size: 20The guard handles an easy trap: typeof null is 'object'. Arrays also need a separate check. Returning a new object copies only the approved fields; unexpected properties do not silently become settings. This example intentionally rejects numeric strings instead of converting them.
Separate syntax errors from invalid settings
Valid JSON can describe a string, number, boolean, array, object, or null. Successful parsing alone therefore cannot prove that input is usable settings. Malformed JSON throws SyntaxError, as specified by ECMAScript’s JSON.parse definition.
Save as handle-json-input.mjs:
import assert from 'node:assert/strict';
function parseInput(text) {
try {
return { ok: true, value: JSON.parse(text) };
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
return { ok: false, message: 'Invalid JSON syntax' };
}
}
const valid = parseInput('null');
const invalid = parseInput('{"enabled":true,}');
assert.deepEqual(valid, { ok: true, value: null });
assert.equal(invalid.ok, false);
assert.equal(parseInput("{'enabled':true}").ok, false);
assert.equal(parseInput('').ok, false);
console.log(`Valid null: ${valid.ok}`);
console.log(invalid.message);node handle-json-input.mjsOutput:
Valid null: true
Invalid JSON syntaxJSON requires double-quoted property names and strings. Trailing commas are invalid. A result object keeps successful null distinct from a failure, and the stable message avoids depending on engine-specific exception wording. In a real application, run your shape checks after a successful parse and show a useful validation message to the caller.
Stringify a deliberate export
Save as write-json-export.mjs:
import assert from 'node:assert/strict';
const record = { name: 'Notebook', quantity: 2, internalNote: 'Check shelf' };
const exported = { name: record.name, quantity: record.quantity };
const text = JSON.stringify(exported, null, 2);
assert.deepEqual(JSON.parse(text), exported);
assert.equal(JSON.stringify({ missing: undefined }), '{}');
assert.equal(JSON.stringify([undefined]), '[null]');
assert.throws(() => JSON.stringify({ id: 1n }), TypeError);
const circular = {};
circular.self = circular;
assert.throws(() => JSON.stringify(circular), TypeError);
console.log(text);node write-json-export.mjsOutput:
{
"name": "Notebook",
"quantity": 2
}The third argument requests two-space indentation. Build the export explicitly when only selected fields belong in the output.
JSON is not a universal JavaScript snapshot: undefined object properties disappear, undefined array elements become null, and ordinary BigInt values and circular references throw. These behaviors follow ECMAScript’s serialization rules. Choose representations deliberately, such as decimal strings for large identifiers, rather than assuming everything will round-trip unchanged.
Clean up
From the scratch directory, remove only the files you created and then remove the empty directory:
rm -- read-json-settings.mjs handle-json-input.mjs write-json-export.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirThe useful habit is simple: parse the syntax, validate the meaning, and serialize an intentional data shape.
Acknowledgements
This post was created with assistance from AI.