JavaScript Essentials

JSON: Parse & Stringify

45 min Lesson 32 of 60

What Is JSON?

JSON stands for JavaScript Object Notation. It is a lightweight, text-based data interchange format that is easy for humans to read and write, and easy for machines to parse and generate. Despite having "JavaScript" in its name, JSON is a language-independent format used across virtually every programming language and platform. It has become the de facto standard for data exchange on the web, powering REST APIs, configuration files, data storage, and communication between clients and servers.

JSON was derived from a subset of JavaScript syntax, and its structure will look immediately familiar to any JavaScript developer. However, JSON has stricter rules than JavaScript objects. Understanding these rules, along with the built-in JSON.parse() and JSON.stringify() methods, is essential for every developer working with web APIs, local storage, configuration files, or any form of structured data exchange.

JSON Format Rules

JSON follows a strict set of formatting rules that distinguish it from JavaScript object syntax. Violating any of these rules will cause parsing to fail. Understanding these rules thoroughly will save you hours of debugging mysterious parsing errors.

Example: JSON vs JavaScript Object Syntax

// Valid JavaScript object -- but NOT valid JSON
const jsObject = {
    name: 'John',          // unquoted keys -- invalid in JSON
    age: 30,
    'is-active': true,
    greet() {               // methods -- not allowed in JSON
        return 'hello';
    },
    hobby: undefined,       // undefined -- not allowed in JSON
    createdAt: new Date(),  // Date objects -- not allowed in JSON
};

// Valid JSON string -- note the differences
const jsonString = `{
    "name": "John",
    "age": 30,
    "isActive": true,
    "hobbies": ["reading", "coding"],
    "address": {
        "city": "New York",
        "country": "USA"
    },
    "spouse": null
}`;

// JSON rules summary:
// 1. Keys MUST be double-quoted strings
// 2. Strings MUST use double quotes (not single quotes)
// 3. No trailing commas allowed
// 4. No comments allowed
// 5. Supported types: string, number, boolean, null, object, array
// 6. NOT supported: undefined, functions, Date, RegExp, Symbol, Infinity, NaN
Note: One of the most common JSON errors is using single quotes instead of double quotes for strings. JSON strictly requires double quotes. Another frequent error is leaving a trailing comma after the last property, which JavaScript allows but JSON does not.

Example: Valid JSON Data Types

// String -- must use double quotes
"Hello, World!"

// Number -- integer or floating point, no leading zeros
42
3.14
-17
2.5e10

// Boolean
true
false

// Null
null

// Object -- ordered collection of key-value pairs
{"key": "value", "count": 5}

// Array -- ordered list of values
[1, "two", true, null, {"nested": "object"}]

// Complex nested structure
{
    "users": [
        {
            "id": 1,
            "name": "Alice",
            "scores": [95, 87, 92],
            "active": true
        },
        {
            "id": 2,
            "name": "Bob",
            "scores": [88, 91, 76],
            "active": false
        }
    ],
    "total": 2,
    "page": 1
}

JSON.parse() -- Converting JSON Strings to JavaScript Values

The JSON.parse() method takes a JSON-formatted string and transforms it into a JavaScript value. This is the primary way you will consume data received from APIs, read from files, or retrieved from local storage. The method takes two arguments: the JSON string to parse and an optional reviver function that can transform values during parsing.

Example: Basic JSON.parse() Usage

// Parsing a simple object
const jsonStr = '{"name": "Alice", "age": 30, "isStudent": false}';
const obj = JSON.parse(jsonStr);

console.log(obj.name);      // "Alice"
console.log(obj.age);       // 30
console.log(obj.isStudent); // false
console.log(typeof obj);    // "object"

// Parsing an array
const arrStr = '[1, 2, 3, "four", true, null]';
const arr = JSON.parse(arrStr);
console.log(arr);            // [1, 2, 3, "four", true, null]
console.log(arr.length);     // 6

// Parsing primitive values
console.log(JSON.parse('42'));       // 42
console.log(JSON.parse('"hello"')); // "hello"
console.log(JSON.parse('true'));     // true
console.log(JSON.parse('null'));     // null

// Parsing nested data
const nested = '{"user": {"name": "Bob", "tags": ["admin", "editor"]}}';
const data = JSON.parse(nested);
console.log(data.user.name);    // "Bob"
console.log(data.user.tags[0]); // "admin"

Example: The Reviver Function

// The reviver function receives each key-value pair during parsing
// It can transform values before they become part of the result

const jsonStr = '{"name": "Alice", "birthDate": "1995-06-15", "score": "85"}';

// Without reviver
const raw = JSON.parse(jsonStr);
console.log(typeof raw.birthDate); // "string"
console.log(typeof raw.score);     // "string"

// With reviver -- convert dates and numbers
const parsed = JSON.parse(jsonStr, (key, value) => {
    // Convert date strings to Date objects
    if (key === 'birthDate') {
        return new Date(value);
    }
    // Convert numeric strings to numbers
    if (key === 'score') {
        return Number(value);
    }
    return value;
});

console.log(parsed.birthDate instanceof Date); // true
console.log(typeof parsed.score);              // "number"

// Reviver for filtering properties
const filtered = JSON.parse(
    '{"name": "Alice", "password": "secret", "email": "alice@example.com"}',
    (key, value) => {
        // Exclude sensitive fields by returning undefined
        if (key === 'password') return undefined;
        return value;
    }
);
console.log(filtered); // {name: "Alice", email: "alice@example.com"}

// Reviver processes nested objects bottom-up
const nestedJson = '{"a": {"b": {"c": 1}}}';
JSON.parse(nestedJson, (key, value) => {
    console.log(key, value);
    return value;
});
// "c" 1
// "b" {c: 1}
// "a" {b: {c: 1}}
// "" {a: {b: {c: 1}}}  (empty key = the root object)

Handling Parse Errors

When JSON.parse() encounters invalid JSON, it throws a SyntaxError. Properly handling these errors is critical in production applications, especially when parsing data from external sources like user input, API responses, or files. You should always wrap JSON.parse() in a try-catch block when dealing with untrusted input.

Example: Robust JSON Parsing

// Common JSON errors and how to handle them
function safeParse(jsonString, fallback = null) {
    try {
        return JSON.parse(jsonString);
    } catch (error) {
        console.error('JSON parse error:', error.message);
        return fallback;
    }
}

// Valid JSON
console.log(safeParse('{"name": "Alice"}'));
// {name: "Alice"}

// Invalid: single quotes
console.log(safeParse("{'name': 'Alice'}"));
// JSON parse error: Unexpected token...
// null

// Invalid: trailing comma
console.log(safeParse('{"name": "Alice",}'));
// JSON parse error: Unexpected token...
// null

// Invalid: undefined value
console.log(safeParse('{"name": undefined}'));
// JSON parse error: Unexpected token...
// null

// Empty string
console.log(safeParse(''));
// JSON parse error: Unexpected end of JSON input
// null

// Using a fallback value
const config = safeParse(localStorage.getItem('settings'), {
    theme: 'light',
    language: 'en'
});
// Returns default settings if stored value is invalid

// Validating JSON structure after parsing
function parseAndValidate(jsonStr, requiredKeys = []) {
    try {
        const data = JSON.parse(jsonStr);
        for (const key of requiredKeys) {
            if (!(key in data)) {
                throw new Error(`Missing required key: ${key}`);
            }
        }
        return { success: true, data };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

const result = parseAndValidate(
    '{"name": "Alice"}',
    ['name', 'email']
);
console.log(result);
// {success: false, error: "Missing required key: email"}
Common Mistake: Never use eval() to parse JSON. While eval() can parse JSON strings, it also executes arbitrary JavaScript code, creating a severe security vulnerability. Always use JSON.parse(), which only parses data and cannot execute code.

JSON.stringify() -- Converting JavaScript Values to JSON Strings

The JSON.stringify() method converts a JavaScript value into a JSON-formatted string. This is how you serialize data for sending to APIs, storing in local storage, or saving to files. The method accepts three arguments: the value to convert, an optional replacer function or array, and an optional space parameter for formatting.

Example: Basic JSON.stringify() Usage

// Stringify a simple object
const user = { name: 'Alice', age: 30, isStudent: false };
const jsonStr = JSON.stringify(user);
console.log(jsonStr);
// '{"name":"Alice","age":30,"isStudent":false}'
console.log(typeof jsonStr); // "string"

// Stringify an array
const arr = [1, 'two', true, null];
console.log(JSON.stringify(arr));
// '[1,"two",true,null]'

// Stringify nested objects
const data = {
    user: { name: 'Bob', tags: ['admin', 'editor'] },
    timestamp: 1700000000
};
console.log(JSON.stringify(data));
// '{"user":{"name":"Bob","tags":["admin","editor"]},"timestamp":1700000000}'

// Stringify primitive values
console.log(JSON.stringify(42));       // "42"
console.log(JSON.stringify('hello')); // '"hello"'
console.log(JSON.stringify(true));     // "true"
console.log(JSON.stringify(null));     // "null"

Example: The space Parameter for Pretty Printing

const data = {
    name: 'Alice',
    age: 30,
    hobbies: ['reading', 'coding'],
    address: {
        city: 'New York',
        country: 'USA'
    }
};

// No formatting (compact)
console.log(JSON.stringify(data));
// {"name":"Alice","age":30,"hobbies":["reading","coding"],"address":{"city":"New York","country":"USA"}}

// With 2-space indentation
console.log(JSON.stringify(data, null, 2));
// {
//   "name": "Alice",
//   "age": 30,
//   "hobbies": [
//     "reading",
//     "coding"
//   ],
//   "address": {
//     "city": "New York",
//     "country": "USA"
//   }
// }

// With 4-space indentation
console.log(JSON.stringify(data, null, 4));

// With tab indentation
console.log(JSON.stringify(data, null, '\t'));

// Custom string separator (up to 10 characters)
console.log(JSON.stringify(data, null, '--'));
// {
// --"name": "Alice",
// --"age": 30,
// ...
// }

Example: The Replacer Function and Array

const user = {
    name: 'Alice',
    password: 'secret123',
    email: 'alice@example.com',
    age: 30,
    role: 'admin'
};

// Replacer as an array -- only include specified keys
const publicJson = JSON.stringify(user, ['name', 'email', 'role']);
console.log(publicJson);
// '{"name":"Alice","email":"alice@example.com","role":"admin"}'

// Replacer as a function -- transform or exclude values
const safeJson = JSON.stringify(user, (key, value) => {
    // Exclude sensitive fields
    if (key === 'password') return undefined;
    // Transform values
    if (key === 'email') return value.replace(/@.*/, '@***');
    return value;
});
console.log(safeJson);
// '{"name":"Alice","email":"alice@***","age":30,"role":"admin"}'

// Replacer function receives nested values too
const nested = {
    level1: {
        level2: {
            secret: 'hidden',
            data: 'visible'
        }
    }
};
const filtered = JSON.stringify(nested, (key, value) => {
    if (key === 'secret') return undefined;
    return value;
}, 2);
console.log(filtered);
// {
//   "level1": {
//     "level2": {
//       "data": "visible"
//     }
//   }
// }

Handling Special Values in JSON

Not all JavaScript values can be represented in JSON. Understanding how JSON.stringify() handles these edge cases is crucial to avoid data loss or unexpected behavior when serializing objects.

Example: How JSON.stringify() Handles Special Values

// undefined -- omitted from objects, converted to null in arrays
console.log(JSON.stringify({ a: undefined, b: 1 }));
// '{"b":1}' -- a is omitted entirely

console.log(JSON.stringify([undefined, 1, undefined]));
// '[null,1,null]' -- replaced with null in arrays

// Functions -- omitted from objects, null in arrays
console.log(JSON.stringify({ greet: function() {} }));
// '{}' -- function is omitted

console.log(JSON.stringify([function() {}, 1]));
// '[null,1]' -- replaced with null

// Symbol -- omitted from objects
console.log(JSON.stringify({ [Symbol('id')]: 1, name: 'test' }));
// '{"name":"test"}' -- Symbol key is omitted

console.log(JSON.stringify({ key: Symbol('val') }));
// '{}' -- Symbol value causes key to be omitted

// Special numbers
console.log(JSON.stringify(Infinity));   // "null"
console.log(JSON.stringify(-Infinity));  // "null"
console.log(JSON.stringify(NaN));        // "null"

// BigInt throws an error
try {
    JSON.stringify(42n);
} catch (e) {
    console.log(e.message);
    // "Do not know how to serialize a BigInt"
}

// Date objects -- converted to their ISO string representation
const date = new Date('2024-01-15T12:00:00Z');
console.log(JSON.stringify(date));
// '"2024-01-15T12:00:00.000Z"'

// RegExp -- converted to empty object
console.log(JSON.stringify(/hello/gi));
// '{}' -- regex becomes empty object

// Map and Set -- converted to empty object
console.log(JSON.stringify(new Map([['a', 1]])));
// '{}' -- Map data is lost

console.log(JSON.stringify(new Set([1, 2, 3])));
// '{}' -- Set data is lost
Pro Tip: When you need to serialize Map, Set, Date, or BigInt values, implement a custom toJSON() method on your objects or use a replacer function in JSON.stringify() combined with a reviver in JSON.parse() to handle the round-trip conversion.

The toJSON() Method

If an object defines a toJSON() method, JSON.stringify() will call it and use the return value for serialization instead of the object itself. This gives you complete control over how your objects are represented in JSON.

Example: Custom toJSON() Method

// Custom class with toJSON
class User {
    constructor(name, email, password) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.createdAt = new Date();
    }

    toJSON() {
        return {
            name: this.name,
            email: this.email,
            createdAt: this.createdAt.toISOString(),
            // password is intentionally excluded
        };
    }
}

const user = new User('Alice', 'alice@example.com', 'secret123');
console.log(JSON.stringify(user, null, 2));
// {
//   "name": "Alice",
//   "email": "alice@example.com",
//   "createdAt": "2024-01-15T12:00:00.000Z"
// }

// Date already has a built-in toJSON() method
const d = new Date('2024-01-15');
console.log(d.toJSON()); // "2024-01-15T00:00:00.000Z"

// Custom serialization for complex types
class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }

    toJSON() {
        return `${this.amount} ${this.currency}`;
    }
}

const price = new Money(29.99, 'USD');
console.log(JSON.stringify({ price }));
// '{"price":"29.99 USD"}'

JSON with Dates

Handling dates is one of the trickiest aspects of working with JSON. Since JSON has no native date type, dates are typically serialized as ISO 8601 strings. You need to explicitly convert them back to Date objects when parsing. This round-trip conversion requires careful handling to avoid bugs.

Example: Date Serialization and Deserialization

// Dates are automatically converted to ISO strings
const event = {
    name: 'Conference',
    startDate: new Date('2024-06-15T09:00:00Z'),
    endDate: new Date('2024-06-17T17:00:00Z')
};

const json = JSON.stringify(event, null, 2);
console.log(json);
// {
//   "name": "Conference",
//   "startDate": "2024-06-15T09:00:00.000Z",
//   "endDate": "2024-06-17T17:00:00.000Z"
// }

// Parsing without reviver -- dates remain as strings
const parsed = JSON.parse(json);
console.log(typeof parsed.startDate); // "string"
console.log(parsed.startDate instanceof Date); // false

// Using a reviver to restore Date objects
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;

const restored = JSON.parse(json, (key, value) => {
    if (typeof value === 'string' && ISO_DATE_REGEX.test(value)) {
        return new Date(value);
    }
    return value;
});

console.log(restored.startDate instanceof Date); // true
console.log(restored.startDate.getFullYear());    // 2024

// Helper function for date-aware parsing
function parseWithDates(jsonStr) {
    return JSON.parse(jsonStr, (key, value) => {
        if (typeof value === 'string') {
            const date = new Date(value);
            if (!isNaN(date.getTime()) && ISO_DATE_REGEX.test(value)) {
                return date;
            }
        }
        return value;
    });
}

Deep Cloning with JSON

A common use of the JSON.parse and JSON.stringify pair is creating deep clones of objects. This technique creates a completely independent copy with no shared references to the original. However, it has limitations that you must understand before using it.

Example: Deep Cloning Objects

// Shallow copy vs deep copy problem
const original = {
    name: 'Alice',
    scores: [95, 87, 92],
    address: { city: 'New York', zip: '10001' }
};

// Shallow copy -- nested objects are still shared
const shallow = { ...original };
shallow.scores.push(100);
console.log(original.scores); // [95, 87, 92, 100] -- original is modified!

// Deep clone with JSON
const deep = JSON.parse(JSON.stringify(original));
deep.scores.push(88);
deep.address.city = 'Boston';
console.log(original.scores);      // [95, 87, 92, 100] -- unchanged
console.log(original.address.city); // "New York" -- unchanged

// Limitations of JSON deep cloning
const problematic = {
    date: new Date(),         // becomes a string
    regex: /pattern/gi,       // becomes {}
    func: function() {},      // is omitted
    undef: undefined,         // is omitted
    nan: NaN,                 // becomes null
    infinity: Infinity,       // becomes null
    map: new Map([['a', 1]]), // becomes {}
    set: new Set([1, 2, 3]),  // becomes {}
};

const cloned = JSON.parse(JSON.stringify(problematic));
console.log(cloned);
// {date: "2024-...", regex: {}, nan: null, infinity: null, map: {}, set: {}}
// func and undef are completely gone!

// Modern alternative: structuredClone (handles more types)
const betterClone = structuredClone(original);
// structuredClone handles Date, RegExp, Map, Set, ArrayBuffer, etc.
// But it still cannot clone functions or DOM elements
Common Mistake: Using JSON.parse(JSON.stringify(obj)) for deep cloning is convenient but loses Date objects, RegExp, Map, Set, functions, and undefined values. For modern JavaScript, prefer structuredClone() which handles more types correctly. Reserve the JSON method for simple data objects containing only JSON-compatible values.

Working with API Responses

One of the most common uses of JSON is communicating with REST APIs. Modern JavaScript uses the fetch() API, which includes built-in methods for parsing JSON responses and sending JSON data. Understanding the full request-response cycle with JSON is essential for any web developer.

Example: Fetching and Sending JSON Data

// Fetching JSON data from an API
async function fetchUsers() {
    try {
        const response = await fetch('https://api.example.com/users');

        // Check if the response is OK (status 200-299)
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        // Parse the JSON response body
        const users = await response.json();
        // response.json() is equivalent to:
        // JSON.parse(await response.text())

        console.log(users);
        return users;
    } catch (error) {
        console.error('Failed to fetch users:', error.message);
        return [];
    }
}

// Sending JSON data to an API
async function createUser(userData) {
    try {
        const response = await fetch('https://api.example.com/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(userData),
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || 'Request failed');
        }

        const newUser = await response.json();
        console.log('Created user:', newUser);
        return newUser;
    } catch (error) {
        console.error('Failed to create user:', error.message);
        return null;
    }
}

// Usage
createUser({
    name: 'Alice',
    email: 'alice@example.com',
    role: 'editor'
});

// Handling paginated API responses
async function fetchAllPages(baseUrl) {
    const allData = [];
    let page = 1;
    let hasMore = true;

    while (hasMore) {
        const response = await fetch(`${baseUrl}?page=${page}&limit=50`);
        const result = await response.json();

        allData.push(...result.data);
        hasMore = result.data.length === 50;
        page++;
    }

    return allData;
}

JSON vs JavaScript Objects

While JSON syntax is based on JavaScript objects, there are important differences that trip up developers regularly. Understanding these differences prevents common errors when switching between the two formats.

Example: Key Differences Between JSON and JavaScript Objects

// 1. Keys must be double-quoted in JSON
// JavaScript: { name: "Alice" } -- valid
// JSON:       { "name": "Alice" } -- must quote keys

// 2. Strings must use double quotes in JSON
// JavaScript: { name: 'Alice' } -- valid with single quotes
// JSON:       { "name": "Alice" } -- only double quotes

// 3. No trailing commas in JSON
// JavaScript: { a: 1, b: 2, } -- valid trailing comma
// JSON:       { "a": 1, "b": 2 } -- NO trailing comma

// 4. No comments in JSON
// JavaScript: { /* comment */ name: "Alice" } -- valid
// JSON:       no comments allowed

// 5. No computed keys in JSON
// JavaScript: { [key]: value } -- valid
// JSON:       keys must be string literals

// 6. Number differences
// JavaScript: 0xFF, 0o77, 0b1010 -- hex, octal, binary
// JSON:       only decimal numbers, no leading zeros

// Quick conversion reference
const obj = { name: 'Alice', age: 30 };

// Object to JSON string
const json = JSON.stringify(obj);

// JSON string to Object
const back = JSON.parse(json);

// Check if two objects are deeply equal using JSON
function deepEqual(obj1, obj2) {
    return JSON.stringify(obj1) === JSON.stringify(obj2);
}
console.log(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })); // true
console.log(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })); // false
// Note: key order matters in JSON comparison!
Note: The deepEqual() function using JSON.stringify is convenient but not perfectly reliable. It depends on key order being consistent and cannot compare objects containing non-JSON-compatible values. For robust deep equality checking, use a library like Lodash or write a recursive comparison function.

localStorage with JSON

The Web Storage API (localStorage and sessionStorage) can only store string values. To store complex data structures like objects and arrays, you must serialize them with JSON.stringify() before saving and deserialize them with JSON.parse() when retrieving. This pattern is fundamental for client-side data persistence.

Example: Storing and Retrieving Complex Data

// Simple wrapper for localStorage with JSON
const storage = {
    set(key, value) {
        try {
            localStorage.setItem(key, JSON.stringify(value));
            return true;
        } catch (error) {
            console.error('Storage set error:', error.message);
            return false;
        }
    },

    get(key, fallback = null) {
        try {
            const item = localStorage.getItem(key);
            return item !== null ? JSON.parse(item) : fallback;
        } catch (error) {
            console.error('Storage get error:', error.message);
            return fallback;
        }
    },

    remove(key) {
        localStorage.removeItem(key);
    },

    has(key) {
        return localStorage.getItem(key) !== null;
    }
};

// Store various data types
storage.set('user', { name: 'Alice', theme: 'dark' });
storage.set('scores', [95, 87, 92, 88]);
storage.set('isLoggedIn', true);
storage.set('lastVisit', new Date().toISOString());

// Retrieve data with type safety
const user = storage.get('user', { name: 'Guest', theme: 'light' });
console.log(user.name);  // "Alice"
console.log(user.theme); // "dark"

const scores = storage.get('scores', []);
console.log(scores.reduce((a, b) => a + b, 0) / scores.length); // average

// Managing a shopping cart
function addToCart(product) {
    const cart = storage.get('cart', []);
    const existing = cart.find(item => item.id === product.id);

    if (existing) {
        existing.quantity += 1;
    } else {
        cart.push({ ...product, quantity: 1 });
    }

    storage.set('cart', cart);
    return cart;
}

function getCartTotal() {
    const cart = storage.get('cart', []);
    return cart.reduce((total, item) => total + item.price * item.quantity, 0);
}

// Managing user preferences
function updatePreferences(updates) {
    const prefs = storage.get('preferences', {
        theme: 'light',
        fontSize: 16,
        language: 'en',
        notifications: true
    });
    const updated = { ...prefs, ...updates };
    storage.set('preferences', updated);
    return updated;
}

Streaming and Large JSON Data

When working with very large JSON datasets, loading everything into memory at once can be problematic. Modern JavaScript provides streaming capabilities and techniques for handling large JSON payloads efficiently. Understanding these patterns is important for building performant applications.

Example: Processing Large JSON Responses

// Streaming JSON with the Fetch API and ReadableStream
async function processLargeResponse(url) {
    const response = await fetch(url);
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        // Process complete JSON lines (NDJSON format)
        const lines = buffer.split('\n');
        buffer = lines.pop(); // keep incomplete line in buffer

        for (const line of lines) {
            if (line.trim()) {
                try {
                    const item = JSON.parse(line);
                    processItem(item);
                } catch (e) {
                    console.warn('Skipping invalid line:', line);
                }
            }
        }
    }
}

function processItem(item) {
    console.log('Processing:', item.id);
}

// Chunked JSON processing for large arrays
function processInChunks(jsonArray, chunkSize, processor) {
    let index = 0;

    function processChunk() {
        const chunk = jsonArray.slice(index, index + chunkSize);
        chunk.forEach(processor);
        index += chunkSize;

        if (index < jsonArray.length) {
            // Yield to the event loop between chunks
            setTimeout(processChunk, 0);
        }
    }

    processChunk();
}

// Usage
const largeArray = new Array(10000).fill(null).map((_, i) => ({
    id: i,
    value: Math.random()
}));

processInChunks(largeArray, 100, (item) => {
    // Process each item without blocking the UI
    document.getElementById('progress').textContent = `Processing ${item.id}...`;
});

// JSON Lines (NDJSON) format -- one JSON object per line
// Useful for streaming and log files
const ndjson = `{"id":1,"name":"Alice"}
{"id":2,"name":"Bob"}
{"id":3,"name":"Charlie"}`;

const records = ndjson
    .split('\n')
    .filter(line => line.trim())
    .map(line => JSON.parse(line));

console.log(records.length); // 3

Practical Data Exchange Examples

Let us look at comprehensive real-world scenarios that combine multiple JSON techniques into practical solutions you will encounter in professional development.

Example: Configuration Management

// Application configuration with defaults and overrides
class Config {
    #defaults;
    #overrides;

    constructor(defaults = {}) {
        this.#defaults = defaults;
        this.#overrides = {};
        this.load();
    }

    load() {
        try {
            const saved = localStorage.getItem('app_config');
            if (saved) {
                this.#overrides = JSON.parse(saved);
            }
        } catch (e) {
            console.warn('Failed to load config, using defaults');
            this.#overrides = {};
        }
    }

    save() {
        localStorage.setItem('app_config', JSON.stringify(this.#overrides));
    }

    get(key) {
        return key in this.#overrides ? this.#overrides[key] : this.#defaults[key];
    }

    set(key, value) {
        this.#overrides[key] = value;
        this.save();
    }

    reset() {
        this.#overrides = {};
        this.save();
    }

    toJSON() {
        return { ...this.#defaults, ...this.#overrides };
    }

    export() {
        return JSON.stringify(this.toJSON(), null, 2);
    }

    import(jsonStr) {
        try {
            this.#overrides = JSON.parse(jsonStr);
            this.save();
            return true;
        } catch (e) {
            console.error('Invalid config format');
            return false;
        }
    }
}

const config = new Config({
    theme: 'light',
    fontSize: 16,
    autoSave: true,
    maxRetries: 3
});

config.set('theme', 'dark');
console.log(config.get('theme')); // "dark"
console.log(config.export());

Example: Data Transformation Pipeline

// Transform API response data into application format
function transformUserResponse(apiJson) {
    const data = typeof apiJson === 'string' ? JSON.parse(apiJson) : apiJson;

    return data.users.map(user => ({
        id: user.id,
        displayName: `${user.first_name} ${user.last_name}`,
        email: user.email_address,
        avatar: user.profile_image_url || '/default-avatar.png',
        joinedAt: new Date(user.created_at),
        isVerified: user.email_verified === true,
        permissions: user.roles.flatMap(role => role.permissions || []),
    }));
}

// API response (typically from fetch().json())
const apiResponse = {
    users: [
        {
            id: 1,
            first_name: 'Alice',
            last_name: 'Johnson',
            email_address: 'alice@example.com',
            profile_image_url: 'https://example.com/alice.jpg',
            created_at: '2024-01-15T12:00:00Z',
            email_verified: true,
            roles: [{ name: 'admin', permissions: ['read', 'write', 'delete'] }]
        }
    ]
};

const users = transformUserResponse(apiResponse);
console.log(JSON.stringify(users, null, 2));

// Serialize for caching
const cacheKey = 'users_cache';
const cacheData = {
    data: users,
    cachedAt: new Date().toISOString(),
    expiresIn: 300000 // 5 minutes
};

localStorage.setItem(cacheKey, JSON.stringify(cacheData, (key, value) => {
    // Convert Date objects to ISO strings for storage
    if (value instanceof Date) return value.toISOString();
    return value;
}));

// Retrieve and validate cache
function getCachedData(key) {
    try {
        const raw = localStorage.getItem(key);
        if (!raw) return null;

        const cache = JSON.parse(raw);
        const cachedTime = new Date(cache.cachedAt).getTime();
        const now = Date.now();

        if (now - cachedTime > cache.expiresIn) {
            localStorage.removeItem(key);
            return null; // cache expired
        }

        return cache.data;
    } catch (e) {
        return null;
    }
}

Example: Form Data Serialization and State Management

// Serialize form data to JSON
function formToJSON(formElement) {
    const formData = new FormData(formElement);
    const json = {};

    for (const [key, value] of formData.entries()) {
        // Handle multiple values (checkboxes, multi-select)
        if (key in json) {
            if (!Array.isArray(json[key])) {
                json[key] = [json[key]];
            }
            json[key].push(value);
        } else {
            json[key] = value;
        }
    }

    return json;
}

// Undo/redo state management with JSON snapshots
class StateManager {
    #history = [];
    #position = -1;
    #maxHistory;

    constructor(initialState = {}, maxHistory = 50) {
        this.#maxHistory = maxHistory;
        this.pushState(initialState);
    }

    get current() {
        return JSON.parse(this.#history[this.#position]);
    }

    pushState(state) {
        // Remove any future states if we branched
        this.#history = this.#history.slice(0, this.#position + 1);

        // Add new state as JSON snapshot
        this.#history.push(JSON.stringify(state));
        this.#position++;

        // Limit history size
        if (this.#history.length > this.#maxHistory) {
            this.#history.shift();
            this.#position--;
        }
    }

    undo() {
        if (this.#position > 0) {
            this.#position--;
            return this.current;
        }
        return null;
    }

    redo() {
        if (this.#position < this.#history.length - 1) {
            this.#position++;
            return this.current;
        }
        return null;
    }

    get canUndo() { return this.#position > 0; }
    get canRedo() { return this.#position < this.#history.length - 1; }
}

// Usage
const state = new StateManager({ items: [], count: 0 });

state.pushState({ items: ['Apple'], count: 1 });
state.pushState({ items: ['Apple', 'Banana'], count: 2 });

console.log(state.current.count); // 2
state.undo();
console.log(state.current.count); // 1
state.redo();
console.log(state.current.count); // 2

Example: Circular Reference Detection

// JSON.stringify throws on circular references
const obj = { name: 'Alice' };
obj.self = obj; // circular reference

try {
    JSON.stringify(obj);
} catch (e) {
    console.log(e.message);
    // "Converting circular structure to JSON"
}

// Custom replacer to handle circular references
function stringifyWithCircular(obj, space = 2) {
    const seen = new WeakSet();

    return JSON.stringify(obj, (key, value) => {
        if (typeof value === 'object' && value !== null) {
            if (seen.has(value)) {
                return '[Circular Reference]';
            }
            seen.add(value);
        }
        return value;
    }, space);
}

const circular = { name: 'Alice', friends: [] };
circular.friends.push(circular); // circular!

console.log(stringifyWithCircular(circular));
// {
//   "name": "Alice",
//   "friends": [
//     "[Circular Reference]"
//   ]
// }

// A more advanced version that shows the path
function safeStringify(obj, space = 2) {
    const seen = new Map();
    let pathIndex = 0;

    return JSON.stringify(obj, function(key, value) {
        if (typeof value === 'object' && value !== null) {
            if (seen.has(value)) {
                return `[Circular: ${seen.get(value)}]`;
            }
            seen.set(value, `#ref${pathIndex++}`);
        }
        return value;
    }, space);
}

Practice Exercise

Build a complete task management system that uses JSON for all data persistence and exchange. Create a TaskManager class that stores tasks in localStorage using JSON serialization. Each task should have an id, title, description, priority (high, medium, low), status (pending, in-progress, done), a created date, and an optional due date. Implement the following features: (1) Add, update, and delete tasks with automatic localStorage persistence. (2) Export all tasks as a formatted JSON string that can be saved as a file. (3) Import tasks from a JSON string with full validation, checking that all required fields exist and have valid values, and handling parse errors gracefully. (4) A search function that filters tasks by title or description and returns matching tasks. (5) A statistics function that returns a JSON object with total tasks, tasks by status, tasks by priority, and overdue tasks count. (6) Implement undo functionality using JSON snapshots of the state so users can reverse their last five actions. (7) Write a data migration function that can read task data in an old format (where priority was a number 1-3) and convert it to the new format (with string priorities). Test all functionality by creating several tasks, modifying them, exporting the data, clearing localStorage, importing the exported data back, and verifying everything matches. Log all results to the console showing the JSON output at each step.