JSON — Reading and Writing Test Fixtures

8 min read

Every API your tests talk to speaks JSON. Every fixture file your tests load is in JSON. The result file your CI pipeline uploads to a dashboard is JSON. JSON is the universal data format of testing — and JavaScript handles it natively, with two built-in functions and a couple of strict rules. This lesson covers what JSON is, how to convert between it and JavaScript objects, and how to read and write JSON files in Node.js.

What JSON is

JSON stands for JavaScript Object Notation. It looks almost identical to a JavaScript object literal — and that resemblance is on purpose; the format was named after JavaScript syntax. But JSON is a string format. Wherever you read JSON, it's text — bytes you load from a file or fetch from a network. To use it as a JavaScript object you have to parse it. To save a JavaScript object as JSON, you stringify it.

The biggest mental shift for beginners: a JSON file is a file full of text. The objects only exist inside your program after you parse it.

JSON's strict rules

JSON looks like a JavaScript object literal, but it's stricter:

  • Keys must be quoted. { "name": "Alice" } ✅. { name: "Alice" } ❌ (in JSON).
  • Strings must use double quotes. 'Alice' is illegal in JSON. Use "Alice".
  • No trailing commas. { "a": 1, } ❌. { "a": 1 } ✅.
  • No comments. Neither // nor /* */.
  • No functions, no undefined. JSON only encodes data: strings, numbers, booleans, null, arrays, and objects.

A JavaScript object literal and its JSON form, side by side:

JavaScript object vs JSON — the differences that matter

JavaScript object

  • Keys can be unquoted: { name: "Alice" }

  • Single or double quotes: 'staging' or "staging"

  • Trailing commas allowed

  • Comments fine: // ...

  • Functions allowed as values

  • undefined is a valid value

JSON

  • Keys MUST be quoted: { "name": "Alice" }

  • Double quotes only: "staging"

  • No trailing commas

  • No comments — at all

  • No functions — data only

  • undefined not allowed — null is

JSON.stringify — object to string

JSON.stringify takes a JavaScript object and returns its JSON representation as a string.

const user = { name: "Alice", role: "admin", verified: true };
 
const json = JSON.stringify(user);
console.log(json);
console.log(typeof json);

Output:

{"name":"Alice","role":"admin","verified":true}
string

The output is one line of dense JSON. Keys are quoted, strings use double quotes, no spaces. Useful for sending over the network where bytes count.

For human-readable output (logs, fixture files, snapshot tests), pass two extra arguments: null for the replacer (advanced — usually null), and a number for the indent:

const user = { name: "Alice", role: "admin", verified: true };
 
console.log(JSON.stringify(user, null, 2));

Output:

{
  "name": "Alice",
  "role": "admin",
  "verified": true
}

2 means "indent with 2 spaces." 4 is also common. This pretty-printed form is what every fixture file should look like — diff-friendly, easy to read, easy to edit by hand.

JSON.parse — string to object

The reverse — JSON.parse takes a JSON string and returns the JavaScript object it describes.

const text = '{"name":"Alice","role":"admin"}';
 
const user = JSON.parse(text);
console.log(user.name);   // "Alice"
console.log(user.role);   // "admin"
console.log(typeof user); // "object"

If the string isn't valid JSON, JSON.parse throws a SyntaxError. In test code it's usually fine to let that crash loudly — a malformed fixture should fail the test immediately, not silently mis-load.

Reading a JSON file in Node.js

Node.js's fs (file system) module reads files. Combine it with JSON.parse and you have a fixture loader:

const fs = require("node:fs");
 
const text = fs.readFileSync("testdata.json", "utf-8");
const data = JSON.parse(text);
 
console.log(`Loaded ${data.users.length} test users`);

"utf-8" tells readFileSync to return a string (the default is a Buffer). For test fixtures, always specify it.

Writing a JSON file

The mirror operation — JSON.stringify, then fs.writeFileSync:

const fs = require("node:fs");
 
const results = [
  { name: "login",    status: "pass" },
  { name: "checkout", status: "fail" }
];
 
fs.writeFileSync("results.json", JSON.stringify(results, null, 2));
console.log("Wrote results.json");

Open the file in any editor and you'll see clean, indented JSON. This is exactly how CI pipelines persist test results between stages.

A real example — load, filter, save

The shape of a real fixture utility — load all users, keep only admins, save the filtered list to a new file.

const fs = require("node:fs");
 
// users.json contains: [{ name: "Alice", role: "admin" }, { name: "Bob", role: "member" }, ...]
const users = JSON.parse(fs.readFileSync("users.json", "utf-8"));
 
const admins = users.filter(u => u.role === "admin");
fs.writeFileSync("admins.json", JSON.stringify(admins, null, 2));
 
console.log(`Wrote ${admins.length} admins to admins.json`);

Three lines that load JSON, transform it, and save it. That same pattern — read fixture, filter or transform, write output — covers a huge fraction of QA scripting.

Validating JSON quickly

When a fixture file has a typo, JSON.parse throws and the error message can be cryptic. The fastest way to find the bad spot is to paste the file into a JSON validator. The JSON Formatter utility on qa.codes parses the input, prints a precise error location if it's invalid, and pretty-prints valid JSON — invaluable when a fixture file silently breaks a test run.

Common JSON errors and how to spot them

  • Trailing comma. { "a": 1, } — JavaScript accepts it; JSON does not. The error usually points at the closing brace.
  • Single quotes. {'name': 'Alice'} — looks fine but isn't valid JSON. Use double quotes everywhere.
  • undefined slipped in. JSON.stringify({ a: undefined }) returns '{}' — the key just disappears. If you need "no value," use null, which JSON understands.
  • Comments. { "a": 1 /* good */ } — JSON has no comments. Strip them out, or use a JSON-with-comments dialect like JSON5 (rarely needed for fixtures).
  • Encoding. A file written with the wrong encoding (UTF-16, BOM-prefixed) may parse as garbage. Save fixtures as plain UTF-8.

⚠️ Common mistakes

  • Forgetting that JSON is a string. console.log(JSON.stringify(obj)) prints text; console.log(obj) prints the object representation. They look similar but behave very differently if you try to read fields off them.
  • Pretty-printing the network payload. JSON.stringify(body, null, 2) is great for log files, but it inflates network requests with spaces. For HTTP requests, use plain JSON.stringify(body).
  • Editing the JSON by hand without a validator. A misplaced comma in a 200-line fixture wastes 30 minutes of tracking down. Run anything you've hand-edited through the JSON Formatter before committing.

🎯 Practice task

Build a fixture-processing utility. 25 minutes.

  1. In your js-for-qa folder, create users.json with an array of 5 user objects, each with name, email, and role (mix of "admin" and "member"):

    [
      { "name": "Alice", "email": "alice@test.com", "role": "admin" },
      { "name": "Bob",   "email": "bob@test.com",   "role": "member" }
    ]

    Add three more so the file has five users total.

  2. Create process.js:

    const fs = require("node:fs");
    const users = JSON.parse(fs.readFileSync("users.json", "utf-8"));
    const admins = users.filter(u => u.role === "admin");
    fs.writeFileSync("admins.json", JSON.stringify(admins, null, 2));
    console.log(`Wrote ${admins.length} admins to admins.json`);
  3. Run with node process.js. Open admins.json and confirm only the admin users are inside.

  4. Deliberately add a trailing comma after the last user in users.json. Rerun. Note the SyntaxError and where it points. Fix it.

  5. Paste the broken JSON into the JSON Formatter at /utilities/json-formatter on qa.codes — see the precise error location.

  6. Stretch: add a third script summary.js that loads admins.json, builds a small object { total: N, names: [...] }, and writes it to summary.json. The result should be a tiny pretty-printed JSON file.

That's the end of chapter 4. You can now load test data from JSON, transform it with arrays and objects, destructure the fields you need, and persist the results — the full data pipeline of a modern test suite. The next chapter introduces the language feature that makes test code work with real APIs and real browsers: asynchronous JavaScript.

// tip to track lessons you complete and pick up where you left off across devices.