Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Creating Test Fixtures for JSON Formatter Validation
If you are testing a JSON formatter, you need more than a handful of pretty examples. A useful fixture set proves three things: valid JSON is accepted, invalid JSON fails for the right reason, and awkward edge cases do not quietly corrupt output.
The fastest way to get there is to build a small, named JSON fixture catalog with clear expectations for formatting, parsing, and failure behavior. That matters because RFC 8259 allows any JSON value at the top level, recommends unique object member names, and notes that some parsers ignore a UTF-8 BOM. Those details shape which fixtures should be strict pass/fail cases and which belong in an interoperability bucket.
Start with a Fixture Contract
Before you collect examples, decide what every fixture must declare. That keeps your tests readable and stops ad hoc edge cases from turning into brittle snapshots.
Recommended minimum shape
type JsonFixture = {
id: string;
bucket: "valid" | "invalid" | "interop";
input: string;
expectedFormatted?: string;
expectedValue?: unknown;
expectedError?: {
type: "SyntaxError";
line?: number;
column?: number;
messageIncludes?: string;
};
notes?: string;
};- Use exact formatted output when your formatter guarantees a canonical style such as 2-space indentation and trailing newline behavior.
- Store parsed value expectations when semantic equality matters more than whitespace.
- Avoid pinning full runtime error messages unless your own library guarantees them. Error wording varies across engines and versions.
Use Three Fixture Buckets
A strong JSON fixture collection is easier to maintain when you separate normal success cases from clear failures and parser-specific gray areas.
1. Valid Fixtures
These should succeed in every standards-compliant parser and formatter. Include both common application data and spec-level coverage.
Useful valid fixtures
{ "name": "Alice", "age": 30 }
[1, 2, 3, false, null, "hello"]
{
"person": {
"name": "Bob",
"address": {
"street": "123 Main St",
"city": "Anytown"
}
},
"items": [
{ "id": 1, "quantity": 10 },
{ "id": 2, "quantity": 5 }
]
}
{ "emptyObject": {}, "emptyArray": [] }
{
"escaped": "quote: \" slash: \\ newline: \n tab: \t",
"unicode": "\u0041\u0042\u2603",
"numberInt": 123,
"numberFloat": -45.67e+8
}
"top-level string"
12345
true
nullTop-level primitives are valid JSON text under RFC 8259, so include them in your passing fixtures.
- Include empty objects, empty arrays, nested arrays, and deep nesting that reflects your real limits.
- Cover control-character escapes, Unicode escapes, and mixed whitespace around commas and colons.
- Prefer unique object member names in strict success fixtures so the expected output stays unambiguous.
2. Invalid Fixtures
These should fail cleanly. They are the fixtures that catch accidental support for JavaScript object literals, comments, or lossy number handling.
Useful invalid fixtures
{ "a": 1, "b": 2, }
[1, 2, 3,]
{ "a" 1 }
{ "a": 1 "b": 2 }
{ key: 1 }
{ 'a': 1 }
{ "a": 01 }
{ "value": NaN }
{ "value": Infinity }
{ "a": "hello\x" }
{ "a": "line1
line2" }
[1 2]
{ "a": 1 } extra_text Raw newlines inside strings, leading zeroes, NaN,Infinity, single quotes, and unquoted keys are not valid JSON.
Keep invalid fixtures as raw text files instead of importing them as JSON modules. That lets you preserve broken syntax exactly as the formatter will receive it.
3. Interoperability Fixtures
Some inputs matter because real parsers disagree. Treat these as their own bucket so you can document the behavior you want instead of pretending the specification settles every detail.
Examples to isolate
{ "dup": 1, "dup": 2 }
\uFEFF{ "bomPrefixed": true }
{ "hugeExponent": 1E400 }
{ "surrogatePair": "\uD834\uDD1E" }Duplicate keys are discouraged by RFC 8259 because receiver behavior is unpredictable, and BOM-prefixed input may be ignored by some parsers. Large-number handling is another common source of cross-runtime surprises.
The open-source JSONTestSuite is useful here because it groups cases into y_ (must parse), n_ (must fail), and i_ (implementation-dependent) files. That naming scheme works well for your own JSON fixtures too.
Organize Fixtures for Raw Input and Expected Output
Separate the raw input from the expectation metadata. That avoids accidental cleanup by editors and makes it obvious which files are safe to pretty-print.
Suggested layout
test/fixtures/
valid/
simple-object.input.json
simple-object.expected.json
top-level-string.input.json
top-level-string.expected.txt
invalid/
trailing-comma.input.txt
trailing-comma.error.json
unquoted-key.input.txt
unquoted-key.error.json
interop/
duplicate-keys.input.txt
duplicate-keys.notes.json
utf8-bom.input.bin
utf8-bom.notes.json- Use
.input.txtfor invalid fixtures so tools do not try to parse or reformat them. - Store formatted expectations separately when your formatter normalizes whitespace or adds a trailing newline.
- Keep notes for interop fixtures so the reason for each parser-specific case is preserved.
Example Test Structure
You do not need a complicated harness. Start with one loop that runs every fixture and branches by bucket.
Vitest example
import { describe, expect, it } from "vitest";
type JsonFixture = {
id: string;
bucket: "valid" | "invalid" | "interop";
input: string;
expectedFormatted?: string;
};
const fixtures: JsonFixture[] = [
{
id: "valid_simple_object",
bucket: "valid",
input: '{"b":2,"a":1}',
expectedFormatted: '{\n "b": 2,\n "a": 1\n}',
},
{
id: "invalid_trailing_comma",
bucket: "invalid",
input: '{"a":1,}',
},
];
function formatJson(input: string) {
return JSON.stringify(JSON.parse(input), null, 2);
}
describe("json formatter fixtures", () => {
for (const fixture of fixtures) {
it(fixture.id, () => {
if (fixture.bucket === "invalid") {
expect(() => formatJson(fixture.input)).toThrow(SyntaxError);
return;
}
const formatted = formatJson(fixture.input);
expect(formatted).toBe(fixture.expectedFormatted);
expect(JSON.parse(formatted)).toStrictEqual(JSON.parse(fixture.input));
});
}
});If your formatter preserves object key order, compare exact strings. If it sorts keys or normalizes line endings, normalize both sides before comparing.
Common Mistakes That Make JSON Fixtures Weak
- Testing only objects and arrays. A JSON text can also be a string, number, boolean, or
null. - Mixing strict and interop cases. Duplicate keys and BOM-prefixed input should not be mixed into ordinary pass/fail assertions without an explicit policy.
- Asserting full engine error text. Syntax messages often change between browsers, Node versions, and libraries.
- Skipping regression fixtures. Every production bug should add one new fixture so the same parser or formatter failure cannot reappear silently.
Practical Checklist
- Create separate
valid,invalid, andinteropfixture buckets. - Include top-level primitives, nested structures, escapes, numbers, and empty containers.
- Store broken JSON as raw text so editor tooling does not “fix” it.
- Assert exact formatted output only where your formatter promises canonical output.
- Add one new fixture whenever you fix a parsing or formatting bug.
Conclusion
Good JSON fixtures are small, explicit, and grouped by intent. When you combine strict valid cases, clear invalid cases, and a separate interoperability bucket, your formatter tests become easier to trust and easier to expand as new bugs appear.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool