Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Test-Driven Development for JSON Formatter Features

Building a reliable JSON formatter isn't just about pretty-printing. It's about correctly handling various data types, nested structures, whitespace, edge cases, and crucially, invalid input without crashing. This is where Test-Driven Development (TDD) shines. TDD provides a structured approach to ensure that each feature of your formatter works exactly as expected, preventing bugs before they even appear.

What is TDD? (The Red-Green-Refactor Cycle)

TDD is a software development process where you write automated tests *before* writing the code they are meant to test. The process follows a simple cycle:

  • Red: Write a small test that fails because the feature it tests doesn't exist yet or doesn't work correctly.
  • Green: Write the minimum amount of code required to make the failing test pass. The goal here is just to pass the test, not perfect code.
  • Refactor: Improve the code you just wrote. Clean it up, make it more efficient, readable, and maintainable, while ensuring all tests (including the new one) still pass.

You repeat this cycle for each small piece of functionality until the formatter is complete.

Why TDD for a JSON Formatter?

JSON seems simple, but formatting involves many subtle details:

  • Handling various data types (`string`, `number`, `boolean`, `null`).
  • Correctly indenting nested objects (`{}`) and arrays (`[]`).
  • Placing commas correctly after elements.
  • Formatting keys and values (e.g., ensuring string keys are quoted).
  • Dealing with empty objects, empty arrays, and nested empty structures.
  • Handling whitespace within the original input (which should often be ignored).
  • Validating input and providing clear error messages for invalid JSON.
  • Optional features like sorting keys alphabetically.

Each of these is a potential source of bugs. TDD forces you to consider these cases upfront and build a robust solution incrementally, with a safety net of tests.

TDD Cycle: Examples for a JSON Formatter

Feature 1: Basic Indentation (Level 1)

Let's start simple: formatting a flat object with two key-value pairs.

Red: Write the failing test

describe('JSON Formatter - Basic Indentation', () => {
  it('should format a simple flat object with 2-space indentation', () => {
    const input = '{"name":"Alice","age":30}';
    const expectedOutput = `{
  "name": "Alice",
  "age": 30
}`;
    expect(formatJson(input, 2)).toBe(expectedOutput);
  });
});

Running this test now will fail, as `formatJson` likely doesn't exist or doesn't produce the expected output yet.

Green: Write just enough code

Implement a basic parser and formatter that can handle a simple flat object, focusing *only* on making the above test pass. Don't worry about nested structures, arrays, or other types yet. Your initial `formatJson` might be very crude.

// In formatter.ts
function formatJson(jsonString: string, indentSpaces: number): string {
  // A very basic implementation just to pass the first test
  // This needs a proper parser ideally, but for GREEN phase,
  // a simple string manipulation might suffice temporarily IF it passes the test.
  // A real implementation would parse first. Let's assume a basic parse function exists.
  try {
    const obj = JSON.parse(jsonString); // Use built-in parser for simplicity in example
    return JSON.stringify(obj, null, indentSpaces); // Built-in stringify with indentation
  } catch (e) {
    throw new Error("Invalid JSON input");
  }
}

// This function is now enough to make the specific test above pass.
// (Note: Using built-in JSON.parse/stringify is cheating for building a *custom* formatter,
// but illustrates the GREEN step conceptually - write *anything* that makes the test pass).
// A true custom formatter would replace JSON.parse/stringify with its own logic.

Run the test again. It should now pass.

Refactor: Improve the code

The current implementation uses `JSON.parse` and `JSON.stringify`, which isn't what you'd do if building a formatter from scratch. In a real scenario, the refactor phase might involve:

  • Structuring the formatter logic (e.g., using a tokenizer and a recursive formatter function).
  • Improving variable names or code clarity.
  • Preparing the code structure for handling more complex cases easily.

After refactoring, run the test again to ensure you haven't broken anything.

Feature 2: Handling Nested Objects

Now, add a test for a slightly more complex structure.

Red: New failing test

describe('JSON Formatter - Nested Structures', () => {
  it('should format a nested object correctly', () => {
    const input = '{"user":{"name":"Bob","address":{"city":"London"}}}';
    const expectedOutput = `{
  "user": {
    "name": "Bob",
    "address": {
      "city": "London"
    }
  }
}`;
    expect(formatJson(input, 2)).toBe(expectedOutput);
  });
});

This new test will fail if your formatter only handled flat structures.

Green: Implement nesting logic

Modify your `formatJson` (or the underlying formatting functions) to recursively handle nested objects, increasing the indentation level for each nested level. Write *just enough* code to make this specific test pass.

// Inside formatJson or a helper formatter function...
// You'd add logic to detect nested objects/arrays and call
// the formatting logic recursively with an increased indent level.
// e.g., a function like formatValue(value, currentIndentLevel)

Run *all* tests. The new nested test should pass, and the previous flat object test must still pass.

Refactor: Clean up nesting code

Refine the recursive formatting logic. Ensure indentation is calculated correctly. Make sure it handles commas properly for the last element in a nested object. Run all tests again.

Feature 3: Handling Arrays and Mixed Types

Add tests for arrays, nested arrays, and objects containing arrays, and different data types.

Red: Add array and type tests

describe('JSON Formatter - Arrays and Types', () => {
  it('should format a simple array', () => {
    const input = '[1,"two",true,null]';
    const expectedOutput = `[
  1,
  "two",
  true,
  null
]`;
    expect(formatJson(input, 2)).toBe(expectedOutput);
  });

  it('should format an object containing an array', () => {
    const input = '{"list":[1,2,{"item":3}]}';
    const expectedOutput = `{
  "list": [
    1,
    2,
    {
      "item": 3
    }
  ]
}`;
    expect(formatJson(input, 2)).toBe(expectedOutput);
  });

  it('should handle different data types correctly', () => {
    const input = '{"num":123.45,"bool":false,"nil":null,"str":"hello"}';
    const expectedOutput = `{
  "num": 123.45,
  "bool": false,
  "nil": null,
  "str": "hello"
}`;
    expect(formatJson(input, 2)).toBe(expectedOutput);
  });
});

These new tests will fail.

Green: Implement array and type handling

Update your formatter to recognize arrays (`[]`), iterate through their elements, handle commas, and apply correct indentation. Ensure it can distinguish and format numbers, booleans, and null correctly (they don't need quotes like strings).

Run all tests. They should all pass now.

Refactor: Refine array and type logic

Review the array formatting code. Is the comma logic clean? Is the type detection robust? Can the value formatting be improved? Run all tests to confirm.

Feature 4: Handling Invalid JSON

A formatter must gracefully handle bad input.

Red: Test for invalid input

describe('JSON Formatter - Error Handling', () => {
  it('should throw an error for invalid JSON', () => {
    const input = '{"name":"Alice", age:30}'; // Missing quotes around 'age'
    expect(() => formatJson(input, 2)).toThrow('Invalid JSON input'); // Or a more specific error message
  });

  it('should throw an error for non-JSON string', () => {
    const input = 'just plain text';
    expect(() => formatJson(input, 2)).toThrow('Invalid JSON input');
  });
});

These tests should fail if your formatter doesn't have input validation, or if it crashes instead of throwing a controlled error.

Green: Add validation

Add validation at the beginning of your `formatJson` function. A simple approach is to use a `try...catch` block around the parsing step.

function formatJson(jsonString: string, indentSpaces: number): string {
  try {
    // Attempt to parse the string first
    const parsed = JSON.parse(jsonString); // Or use your custom parser
    // If parsing succeeds, proceed to format the parsed object/array
    return myCustomFormatLogic(parsed, indentSpaces);
  } catch (e) {
    // If parsing fails, throw a user-friendly error
    throw new Error("Invalid JSON input");
  }
}

Run all tests. The new error tests should pass, and existing tests should still pass (as valid JSON is handled).

Refactor: Improve error reporting

Can you make the error message more specific? Include the line or column number where the parsing failed? This would require integrating error reporting into your custom parser (if not using `JSON.parse`). Run tests again.

More Test Ideas (Edge Cases & Features)

As you continue, consider writing tests for:

  • Empty objects: {}
  • Empty arrays: []
  • Nested empty objects/arrays: {"a":{},"b":[]}
  • JSON that is just a primitive (number, string, boolean, null): `"hello"`, `123`, `true`, `null`
  • Different indentation levels (e.g., 4 spaces, tabs) - parameterize your tests.
  • JSON strings containing escaped characters (`\n`, `\"`, `\\`, `\uXXXX`).
  • Very large numbers or numbers with high precision.
  • Extremely deeply nested structures (might require adjusting recursion limits if not using a stack-based approach).
  • Input with leading/trailing whitespace.
  • Optional features like sorting object keys alphabetically.

Benefits of this TDD Approach

  • Forces Clear Requirements: Writing the test first makes you define exactly what the code should do.
  • Reduces Bugs: You catch issues early, especially edge cases you might otherwise miss.
  • Provides a Safety Net: When you refactor or add new features, you can run all previous tests to ensure nothing is broken.
  • Improves Design: Code written with testability in mind often has a cleaner, more modular design.
  • Increases Confidence: A comprehensive suite of passing tests gives you confidence that your formatter works correctly.

Potential Pitfalls

While beneficial, be mindful of:

  • Testing Implementation vs. Behavior: Focus tests on *what* the formatter outputs for a given input, not *how* it generates the output (e.g., don't test internal private methods unless necessary).
  • Test Granularity: Keep tests small and focused on a single aspect or feature.
  • Over-testing Trivial Code: Sometimes simple code doesn't need a complex test, but for something with many interactions and edge cases like a formatter, testing is high value.

Conclusion

Applying Test-Driven Development to building a JSON formatter is an excellent way to approach a problem with numerous small requirements and potential edge cases. By following the Red-Green-Refactor cycle and systematically adding tests for each feature and data type, you can build a robust, reliable, and maintainable formatting tool with confidence. The test suite becomes living documentation of your formatter's capabilities.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool