JSON Syntax Explained: Objects, Arrays, and the Mistakes That Break APIs
- The no-trailing-comma rule is permanent and non-negotiable β memorise it, lint for it, and save yourself hours of debugging parser errors that point to the closing brace instead of the actual offending comma.
- JSON.stringify() silently drops undefined values and functions without throwing β your object shape and your JSON shape can diverge without any error, which is why you validate your serialised output in tests, not just your source objects.
- Return [] not null for empty lists β every API that returns null for an empty collection forces every consumer to add a defensive null check, and someone will always forget, causing a TypeError in production.
A single trailing comma in a JSON config file took down a fintech startup's entire deployment pipeline for four hours. Not a logic error. Not a race condition. One comma after the last property in an object β invisible to the eye, fatal to the parser. That's the world you're stepping into.
JSON β JavaScript Object Notation β is the lingua franca of the modern web. REST APIs send it. Configuration files are written in it. Databases like MongoDB store it. Mobile apps receive it. You cannot build anything networked in 2024 without touching JSON constantly. The problem isn't that it's complicated. It's that it looks deceptively simple, so people get sloppy, and sloppy JSON doesn't degrade gracefully β it throws a hard error and stops everything cold.
By the end of this, you'll be able to write valid JSON from scratch without second-guessing yourself, read a raw JSON payload and instantly spot what's wrong with it, understand exactly why each syntax rule exists, and debug the specific parser errors that make junior devs spend an hour staring at code that looks perfectly fine.
What JSON Actually Is β and Why the Rules Are So Unforgiving
Before JSON existed, developers exchanged data using XML. It looked like HTML β verbose, nested tags everywhere, an absolute nightmare to parse and read. A simple user record might take 20 lines of XML. The same data in JSON takes 5. JSON was designed by Douglas Crockford in the early 2000s as a minimal, human-readable data format that any language could parse with a trivial amount of code.
The strictness isn't arbitrary bureaucracy. JSON is designed to be parsed by machines across every programming language on the planet. If the format allowed ambiguity β JavaScript-style trailing commas, single quotes, unquoted keys β every parser would need to handle edge cases differently. Two services would argue about what a payload means. Data corruption follows. The strict rules are what make universal interoperability possible.
JSON only knows six value types: strings, numbers, booleans (true/false), null, objects, and arrays. That's it. No dates. No functions. No undefined. No comments. If you're trying to put a JavaScript Date object directly into JSON, you're going to have a bad time β and we'll cover that. First, understand the two structural building blocks everything else is built from: objects and arrays.
// io.thecodeforge β JavaScript tutorial // The six legal JSON value types β know these cold. // Every valid JSON document is built from exactly these primitives. const validJsonValues = { // 1. String β MUST use double quotes. Single quotes = invalid JSON. productName: "Running Shoe", // 2. Number β integer or decimal, no quotes around it. priceInCents: 9999, weightKg: 0.85, // 3. Boolean β lowercase only. True/False with capitals = invalid JSON. inStock: true, onSale: false, // 4. Null β lowercase only. Represents intentional absence of a value. discountCode: null, // 5. Object β a nested collection of key-value pairs (covered next section). dimensions: { lengthCm: 30, widthCm: 12 }, // 6. Array β an ordered list of values (covered after objects). availableSizes: [7, 8, 9, 10, 11] }; // Convert a JavaScript object to a JSON string β this is what gets sent over the wire. const jsonString = JSON.stringify(validJsonValues, null, 2); console.log(jsonString); // Parse a JSON string back into a JavaScript object β this is what your API receives. const parsedBack = JSON.parse(jsonString); console.log(parsedBack.productName); // "Running Shoe" console.log(typeof parsedBack.inStock); // "boolean" β not a string
"productName": "Running Shoe",
"priceInCents": 9999,
"weightKg": 0.85,
"inStock": true,
"onSale": false,
"discountCode": null,
"dimensions": {
"lengthCm": 30,
"widthCm": 12
},
"availableSizes": [
7,
8,
9,
10,
11
]
}
Running Shoe
boolean
JSON Objects: The Key-Value Store That Powers Every API Response
A JSON object is a collection of key-value pairs wrapped in curly braces. The key is always a double-quoted string. The value is any of the six legal JSON types. Key and value are separated by a colon. Pairs are separated by commas. The last pair gets no trailing comma β this is the rule that bites people constantly.
Why no trailing comma? Because JSON was designed to be a strict subset of a specific version of JavaScript from 2001. Trailing commas weren't valid JavaScript then. The spec was frozen, and that decision became permanent. Every JSON parser in every language since then has inherited this constraint. Like it or not, that's the deal.
Objects can nest inside objects. A user object can contain an address object. That address object can contain a geo object. There's no technical depth limit β but if you're nesting more than three or four levels deep in a production API response, that's a design smell. You're probably shipping more structure than the client needs, which wastes bandwidth and makes the payload harder to consume.
// io.thecodeforge β JavaScript tutorial // Real-world scenario: a checkout service returns this order confirmation // payload to the frontend after a successful purchase. const orderConfirmation = { "orderId": "ORD-2024-88421", "status": "confirmed", "totalAmountCents": 15498, "currency": "USD", // Nested object β customer info lives inside the order object. "customer": { "customerId": "USR-10042", "email": "alex.morgan@example.com", "loyaltyTier": "gold" }, // Nested object with its own nested object β shipping details. "shippingAddress": { "street": "742 Evergreen Terrace", "city": "Springfield", "stateCode": "IL", "postalCode": "62701", // Nested geo coordinates for the delivery routing service. "geo": { "latitude": 39.7817, "longitude": -89.6501 } }, // Boolean flag the frontend uses to decide whether to show a gift message UI. "isGiftOrder": false, // Null means no promo was applied β explicitly stated, not just absent. "promoCode": null // ^^^ No trailing comma here. This is the last property. Add one and JSON.parse blows up. }; // Access nested properties using dot notation after parsing. const jsonPayload = JSON.stringify(orderConfirmation); const parsed = JSON.parse(jsonPayload); console.log(parsed.customer.email); // "alex.morgan@example.com" console.log(parsed.shippingAddress.geo.latitude); // 39.7817 console.log(parsed.promoCode); // null console.log(parsed.promoCode === null); // true β null is explicit, not undefined
39.7817
null
true
JSON Arrays: Ordered Lists and the Gotchas Hidden Inside Them
A JSON array is an ordered, comma-separated list of values wrapped in square brackets. The values don't have to be the same type β an array can hold strings, numbers, objects, other arrays, nulls, whatever you want. In practice, mixing types in a production array is a terrible idea because every consumer has to handle it defensively, but the spec allows it.
Arrays are zero-indexed. The first item is at index 0. This matters when you're debugging a parser error and the error message tells you the problem is 'at position 0 in the array' β it means the first element.
Where arrays get genuinely tricky in production is arrays of objects. This is the pattern behind almost every list endpoint in any REST API you'll ever call. A GET /products endpoint returns an array of product objects. A GET /orders endpoint returns an array of order objects. Each object in the array must individually follow all JSON object rules β double-quoted keys, no trailing commas, no comments. I've seen a team waste a full sprint debugging an import feature because a third-party vendor was sending an array where one object out of 500 had a trailing comma. The other 499 parsed fine. That one object silently corrupted the batch.
// io.thecodeforge β JavaScript tutorial // Real-world scenario: a product catalog API returns a page of results. // This is the most common JSON shape you'll encounter β an array of objects. const catalogPage = { "page": 1, "pageSize": 3, "totalResults": 1482, // Array of objects β each element is a complete product record. "products": [ { "productId": "SKU-001", "name": "Trail Runner X9", "priceInCents": 12999, "categories": ["footwear", "outdoor", "running"], // Array inside an object inside an array. "available": true }, { "productId": "SKU-002", "name": "Merino Wool Sock Pack", "priceInCents": 2499, "categories": ["footwear", "accessories"], "available": true }, { "productId": "SKU-003", "name": "Compression Sleeve", "priceInCents": 1799, "categories": ["recovery"], "available": false // No trailing comma β this is the last property in the last object. } // No trailing comma after the last object in the array either. ] }; const jsonString = JSON.stringify(catalogPage, null, 2); const parsed = JSON.parse(jsonString); // Iterate over the array of objects β the bread and butter of frontend development. parsed.products.forEach((product, index) => { // Template literals for readable output β note the zero-based index. console.log(`[${index}] ${product.name} β $${(product.priceInCents / 100).toFixed(2)} β ${product.available ? 'In Stock' : 'Out of Stock'}`); }); // Safely access a nested array inside an object inside an array. console.log(parsed.products[0].categories[1]); // "outdoor" β zero-indexed all the way down
[1] Merino Wool Sock Pack β $24.99 β In Stock
[2] Compression Sleeve β $17.99 β Out of Stock
outdoor
The Exact JSON Errors That Break Production Code β and How to Fix Them
JSON errors aren't subtle. The parser hits something invalid and throws immediately with a SyntaxError. The problem is the error message tells you where in the string the parser gave up β not where the actual mistake is. If your JSON is 800 lines long and the error says 'position 4721', good luck finding it without knowing what to look for.
Here are the six mistakes I've personally seen break production systems. Not hypothetical mistakes β real incidents, real error messages, real fixes. These aren't sorted by frequency. They're sorted by how long it takes a junior developer to spot them without knowing they exist.
The nastiest one isn't a syntax error at all. It's type coercion β sending a price as the string '1999' instead of the number 1999. The JSON is perfectly valid, it parses without error, and then your checkout service calculates a total of '1999' + '499' = '1999499' instead of 2498. That specific bug caused a real e-commerce platform to charge customers the wrong amount for six hours before anyone noticed. No error. No alert. Just wrong numbers.
// io.thecodeforge β JavaScript tutorial // Six real JSON mistakes with their exact error messages and fixes. // Run each JSON.parse() call individually to see the error β they're isolated below. // βββ MISTAKE 1: Trailing comma after the last property βββββββββββββββββββββββ const trailingComma = '{"name": "Alice", "age": 30,}'; // SyntaxError: Unexpected token } in JSON at position 28 // Fix: Remove the comma after 30. JSON doesn't allow trailing commas. Ever. try { JSON.parse(trailingComma); } catch (e) { console.log('Mistake 1:', e.message); } // βββ MISTAKE 2: Single quotes instead of double quotes βββββββββββββββββββββββ const singleQuotes = "{'name': 'Alice'}"; // SyntaxError: Unexpected token ' in JSON at position 1 // Fix: Replace all single quotes with double quotes. try { JSON.parse(singleQuotes); } catch (e) { console.log('Mistake 2:', e.message); } // βββ MISTAKE 3: Unquoted key βββββββββββββββββββββββββββββββββββββββββββββββββ const unquotedKey = '{name: "Alice"}'; // SyntaxError: Unexpected token n in JSON at position 1 // Fix: Wrap the key in double quotes: {"name": "Alice"} try { JSON.parse(unquotedKey); } catch (e) { console.log('Mistake 3:', e.message); } // βββ MISTAKE 4: Comment inside JSON ββββββββββββββββββββββββββββββββββββββββββ const withComment = '{"name": "Alice" /* the admin user */}'; // SyntaxError: Unexpected token / in JSON at position 17 // Fix: JSON has no comment syntax. Strip all comments before parsing. try { JSON.parse(withComment); } catch (e) { console.log('Mistake 4:', e.message); } // βββ MISTAKE 5: undefined as a value βββββββββββββββββββββββββββββββββββββββββ // undefined is a JavaScript concept β it doesn't exist in JSON. // JSON.stringify() silently drops keys with undefined values. const objectWithUndefined = { username: "alice", sessionToken: undefined // This key will vanish during serialisation. }; const serialised = JSON.stringify(objectWithUndefined); console.log('Mistake 5 β undefined silently dropped:', serialised); // Output: {"username":"alice"} β sessionToken is GONE. No error. No warning. // Fix: Use null for intentional absence. Use a default string like "" if the // field must always be present. // βββ MISTAKE 6: Type confusion β price as string instead of number ββββββββββββ const orderA = JSON.parse('{"itemPriceCents": "1999"}'); // String β looks fine. const orderB = JSON.parse('{"itemPriceCents": 499}'); // Number β correct. // This is the silent killer. No parse error. Wrong result. const wrongTotal = orderA.itemPriceCents + orderB.itemPriceCents; console.log('Mistake 6 β string + number:', wrongTotal); // '1999499' β not 2498! // Fix: Always validate and coerce types on ingestion. const correctTotal = Number(orderA.itemPriceCents) + orderB.itemPriceCents; console.log('Mistake 6 β fixed:', correctTotal); // 2498
Mistake 2: Unexpected token '\'' is not valid JSON
Mistake 3: Unexpected token 'n' is not valid JSON
Mistake 4: Unexpected token '/' is not valid JSON
Mistake 5 β undefined silently dropped: {"username":"alice"}
Mistake 6 β string + number: 1999499
Mistake 6 β fixed: 2498
| Aspect | JSON Object {} | JSON Array [] |
|---|---|---|
| Structure | Key-value pairs β each value has a name | Ordered list β values accessed by numeric index |
| Key requirement | Keys must be double-quoted strings | No keys β position is the identifier |
| Order guarantee | Order not guaranteed by spec (though most parsers preserve it) | Order is guaranteed and meaningful |
| Access pattern | parsed.customer.email | parsed.products[0].name |
| Best for | A single entity with named properties (one user, one order) | Multiple entities of the same type (list of users, list of orders) |
| Empty state | {} β empty object, zero properties | [] β empty array, zero elements |
| Nested inside each other | Objects can contain arrays as property values | Arrays can contain objects as elements |
| Type mixing allowed | Each property can have a different type | Elements can be different types β but don't do it in production |
π― Key Takeaways
- The no-trailing-comma rule is permanent and non-negotiable β memorise it, lint for it, and save yourself hours of debugging parser errors that point to the closing brace instead of the actual offending comma.
- JSON.stringify() silently drops undefined values and functions without throwing β your object shape and your JSON shape can diverge without any error, which is why you validate your serialised output in tests, not just your source objects.
- Return [] not null for empty lists β every API that returns null for an empty collection forces every consumer to add a defensive null check, and someone will always forget, causing a TypeError in production.
- Valid JSON is not the same as correct JSON β a price field serialised as the string '9999' instead of the number 9999 parses without error but will corrupt any arithmetic downstream, which is why schema validation with Zod or Ajv at API boundaries is non-optional on production systems.
β Common Mistakes to Avoid
- βMistake 1: Adding a trailing comma after the last property in an object or the last element in an array β SyntaxError: Unexpected token } in JSON at position N β Remove the trailing comma; JSON.parse() enforces this with zero tolerance, unlike JavaScript which allows it in object literals since ES5.
- βMistake 2: Using single quotes for keys or string values instead of double quotes β SyntaxError: Unexpected token ' in JSON at position 1 β Do a global find-and-replace for single-quoted strings before parsing, or use a linter like eslint-plugin-json that catches this at write time.
- βMistake 3: Passing a JavaScript object with undefined values through JSON.stringify() and assuming all keys survive β No error thrown, keys with undefined values are silently dropped from the output β Audit the stringified result explicitly in tests, or replace undefined with null using a replacer function: JSON.stringify(obj, (key, val) => val === undefined ? null : val).
- βMistake 4: Storing numbers as strings in JSON payloads ('priceInCents': '1999' instead of 'priceInCents': 1999) β No parse error but arithmetic produces string concatenation instead of addition, e.g. '1999' + 499 = '1999499' β Enforce schema validation with a library like Zod or Ajv on every API boundary so type mismatches are caught on ingestion, not in a post-mortem.
- βMistake 5: Trying to include a JavaScript Date object directly in JSON β JSON.stringify() converts it to an ISO 8601 string automatically, but JSON.parse() brings it back as a plain string, not a Date β Always parse date strings explicitly: new Date(parsed.createdAt), and document in your API contract that all timestamps are ISO 8601 strings.
Interview Questions on This Topic
- QJSON.stringify() and JSON.parse() seem symmetrical β but what data types does stringify silently transform or drop that parse can never recover, and how would you design a serialisation layer that handles those cases safely?
- QYour team receives a third-party webhook payload that's technically valid JSON but uses inconsistent types β sometimes a price field is a number, sometimes a string. You can't change the vendor. How do you defend your service against this at the boundary without polluting business logic throughout your codebase?
- QWhat happens when you call JSON.parse() on a deeply nested JSON string with circular references after accidentally running JSON.stringify() on a JavaScript object that contains circular references β and what's the exact error you'd see at each stage?
Frequently Asked Questions
Why does JSON.parse keep throwing SyntaxError even though my JSON looks correct?
The most likely culprit is a trailing comma after the last property in an object or array β JSON.parse('{"name": "Alice",}') throws even though JavaScript itself allows trailing commas in object literals. Paste your JSON into jsonlint.com, which points to the exact line and character of the violation. The second most common cause is single quotes β JSON requires double quotes everywhere, no exceptions.
What's the difference between a JSON object and a JSON array?
An object is a named collection β you access values by their key, like parsed.username. An array is an ordered list β you access values by their position, like parsed.items[0]. Use an object when you're describing a single thing with properties. Use an array when you have multiple things of the same kind.
How do I handle a JavaScript Date object in JSON since JSON doesn't have a date type?
JSON.stringify() automatically converts Date objects to ISO 8601 strings like '2024-03-15T09:30:00.000Z'. JSON.parse() brings that back as a plain string, not a Date object. You have to explicitly reconstruct it: const createdAt = new Date(parsed.createdAt). Document in your API contract that all timestamps are ISO 8601 UTC strings and parse them at the boundary β never deeper in your business logic.
Can JSON handle circular references, and what happens in production if an object with one gets serialised?
JSON.stringify() throws TypeError: Converting circular structure to JSON the moment it encounters a circular reference β it doesn't produce partial output, it just crashes. This has burned teams who pass Express request or response objects into a logger that calls JSON.stringify() internally. The fix is either to sanitise the object before logging using a library like flatted or safe-json-stringify, or to use structured logging libraries like pino that handle circular references internally.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.