JSON looks simple — just key-value pairs, right? But its strictness is legendary. No trailing commas. No comments. No single quotes. No undefined. Miss one rule and your entire payload is dead on arrival.
Even senior developers get caught by these. Not because they don't know JSON — because the spec is unforgiving in ways that feel arbitrary until you understand why those rules exist.
This article covers the 10 most common JSON errors, why they happen, and exactly how to fix them — with real-world code examples, not toy snippets.
Quick Reference Table
| # | Error | The Fix |
|---|---|---|
| 1 | Trailing commas | Remove the last comma in arrays/objects |
| 2 | Single quotes | Use double quotes " for all keys and values |
| 3 |
undefined / NaN / Infinity
|
Replace with null or a valid number |
| 4 | Unescaped special characters | Escape ", \, and control chars in strings |
| 5 | Mismatched brackets | Lint your JSON before sending |
| 6 | Comments in JSON | Strip comments; use JSON5 if you need them |
| 7 | Wrong data types | Validate types at the boundary — never silently coerce |
| 8 | Circular references | Use a replacer function or the flatted library |
| 9 | Date objects | Store as ISO strings; parse explicitly on the way out |
| 10 | Large integers | Use strings or BigInt for numbers > 2⁵³ − 1 |
Why Does JSON Keep Tripping Developers Up?
JSON was designed as a minimal, language-agnostic data format. That minimalism is intentional — but it means there's almost zero forgiveness baked in.
Unlike JavaScript (which has semicolon insertion, loose equality, and other guardrails), JSON has zero tolerance for syntax deviations.
The other issue: most JSON errors surface at runtime — often in production, often deep inside a third-party API response you didn't write. Editor linting helps, but it can't catch errors in dynamically generated payloads.
That's what this guide is for.
Error #1: Trailing Commas That Kill Your Parser
What's happening
A trailing comma is a comma after the last item in an array or object. JavaScript has allowed this since ES5. JSON absolutely does not.
The error you'll see
SyntaxError: Unexpected token } in JSON at position 42
❌ Wrong
{
"user": {
"name": "Priya Kapoor",
"role": "admin",
"permissions": ["read", "write", "delete",]
},
}
Two trailing commas here: after "delete" and after the closing } of user.
✅ Fixed
{
"user": {
"name": "Priya Kapoor",
"role": "admin",
"permissions": ["read", "write", "delete"]
}
}
Remove the last comma from every array and object. Most JSON linters flag this instantly.
💡 Pro tip: Never hand-edit minified JSON. Always pretty-print first (
JSON.stringify(obj, null, 2)), edit, then re-minify. The structure is visible and trailing commas are easy to catch.
Error #2: Single Quotes Instead of Double Quotes
What's happening
JavaScript lets you use single quotes, double quotes, or backticks for strings. JSON only allows double quotes — for both keys and string values.
The error you'll see
SyntaxError: Unexpected token ' in JSON at position 0
❌ Wrong
{
'endpoint': '/api/v2/orders',
'timeout': '30s'
}
✅ Fixed
{
"endpoint": "/api/v2/orders",
"timeout": "30s"
}
The sneaky variant
This bites developers who copy a JavaScript object literal and paste it into a JSON file:
// Valid JS object literal
const config = {
host: 'localhost',
port: 5432
}
That object has unquoted keys — which is also invalid JSON, even if you swap in double quotes. Both the key format and the string delimiters need to change.
💡 Pro tip: If you accept JSON from users or external systems, always wrap
JSON.parse()in atry/catch. Never assume the format is correct.
Error #3: undefined, NaN, and Infinity Aren't Valid JSON
What's happening
JavaScript has special values — undefined, NaN, Infinity, -Infinity — that simply don't exist in the JSON spec. When you serialize an object containing these, JSON.stringify() silently drops or converts them.
The error you'll see
No error. That's the problem. The values just vanish silently.
const payload = {
userId: 42,
score: NaN,
bonus: Infinity,
metadata: undefined
};
console.log(JSON.stringify(payload));
// {"userId":42,"score":null,"bonus":null}
// ⚠️ metadata key is gone entirely
❌ Wrong
const stats = {
avgResponseTime: totalTime / requestCount, // 💥 NaN if requestCount === 0
maxRetries: Infinity
};
fetch('/api/stats', {
method: 'POST',
body: JSON.stringify(stats)
});
// Server receives: {"avgResponseTime":null,"maxRetries":null}
✅ Fixed
function sanitizeForJSON(value) {
if (typeof value !== 'number') return value;
if (Number.isNaN(value) || !Number.isFinite(value)) return null;
return value;
}
const stats = {
avgResponseTime: sanitizeForJSON(totalTime / requestCount) ?? 0,
maxRetries: 100 // Use a real cap, not Infinity
};
Or use a replacer with JSON.stringify:
const safeStringify = (obj) =>
JSON.stringify(obj, (key, value) => {
if (typeof value === 'number' && !Number.isFinite(value)) return null;
if (value === undefined) return null;
return value;
});
💡 Pro tip:
JSON.stringifyalso silently drops functions andSymbolvalues. If your object might contain these, validate the output or use a schema validator likezodorajvbefore sending.
Error #4: Unescaped Special Characters in Strings
What's happening
Certain characters inside JSON strings must be escaped with a backslash. The most common culprits: double quotes, backslashes, and control characters like newlines and tabs.
The error you'll see
SyntaxError: Bad escaped character in JSON at position 34
Or a subtler one — a string that ends prematurely because an unescaped " closes it early.
❌ Wrong
{
"message": "He said "hello" and left",
"filePath": "C:\Users\priya\documents\report.pdf",
"notes": "First line
Second line"
}
Three problems here:
- Unescaped
"inside a string - Unescaped backslashes in the Windows path
- Literal newline inside a string
✅ Fixed
{
"message": "He said \"hello\" and left",
"filePath": "C:\\Users\\priya\\documents\\report.pdf",
"notes": "First line\nSecond line"
}
Full escape reference:
| Character | Escaped |
|---|---|
" |
\" |
\ |
\\ |
| Newline | \n |
| Tab | \t |
| Carriage return | \r |
| Unicode | \uXXXX |
The golden rule for dynamic JSON
If you're building JSON strings from user input, always use JSON.stringify() — never concatenate strings manually:
// ❌ Never do this — JSON injection + XSS risk
const json = `{"message": "${userInput}"}`;
// ✅ Do this
const json = JSON.stringify({ message: userInput });
💡 Pro tip: Windows file paths are the #1 source of unescaped backslash bugs. If you're storing paths in JSON config files, always double-escape them or use forward slashes — Windows accepts both.
Error #5: Mismatched Brackets or Braces
What's happening
Every { needs a }. Every [ needs a ]. In long or deeply nested JSON, a missing bracket is easy to introduce and hard to spot manually.
The error you'll see
SyntaxError: Unexpected end of JSON input
or
SyntaxError: Expected ',' or '}' after property value in JSON at position 312
❌ Wrong
{
"pipeline": {
"stages": [
{
"name": "build",
"steps": ["install", "compile", "test"
},
{
"name": "deploy",
"steps": ["push", "restart"]
}
]
}
}
The steps array in the first stage is missing its closing ].
✅ Fixed
{
"pipeline": {
"stages": [
{
"name": "build",
"steps": ["install", "compile", "test"]
},
{
"name": "deploy",
"steps": ["push", "restart"]
}
]
}
}
How to find it fast
Paste your JSON into an online JSON formatter — it highlights exactly which line the error is on and pretty-prints your JSON so the structure is obvious. You can also run a quick check in Node:
node -e "JSON.parse(require('fs').readFileSync('config.json', 'utf8'))" && echo "Valid"
Or use jq:
cat data.json | jq .
💡 Pro tip: When writing large JSON by hand, add both the opening and closing bracket before filling in the contents. Same principle as closing HTML tags immediately — far less error-prone than adding the closer after 30 lines of content.
Error #6: Comments in JSON — The Feature That Doesn't Exist
What's happening
Developers coming from JavaScript, Python, or YAML naturally try to add comments to JSON config files. JSON has no comment syntax. None. Not //, not /* */, not #.
The error you'll see
SyntaxError: Unexpected token / in JSON at position 2
❌ Wrong
{
// Database connection settings
"host": "db.internal.example.com",
"port": 5432, /* default Postgres port */
"poolSize": 10 # max concurrent connections
}
✅ Fix option 1: Remove comments
{
"host": "db.internal.example.com",
"port": 5432,
"poolSize": 10
}
✅ Fix option 2: Use a _comment convention
Some teams use a dedicated key for inline documentation:
{
"_comment": "Database settings — see runbook for rotation procedure",
"host": "db.internal.example.com",
"port": 5432,
"poolSize": 10
}
Not elegant, but it parses cleanly and is searchable.
✅ Fix option 3: Switch to JSON5 or JSONC
If you control the parsing (e.g., a config file for your own tool), use JSON5 or JSONC:
import JSON5 from 'json5';
const config = JSON5.parse(`{
// Database connection
host: "db.internal.example.com",
port: 5432,
}`);
💡 Pro tip:
tsconfig.json,.eslintrc.json, and VS Code'ssettings.jsonall silently support comments even though they use.json— VS Code's parser strips them. Don't let this fool you into thinking regularJSON.parse()handles them. It won't.
Error #7: Incorrect Data Types Causing Silent Failures
What's happening
JSON supports six types: strings, numbers, booleans, arrays, objects, and null. The problem isn't usually an invalid type — it's passing the right value in the wrong type, especially numbers as strings or booleans as strings.
The error you'll see
No parsing error at all. You get a logic bug — comparisons fail, math returns NaN, or an API returns a cryptic 400 Bad Request.
❌ Wrong
// HTML form data — everything comes back as strings
const formData = {
userId: "1042", // Should be a number
isAdmin: "false", // Should be a boolean — but "false" is truthy!
retryCount: "3" // Should be a number
};
// On the receiving end:
if (data.isAdmin) {
// "false" is a non-empty string → truthy → this ALWAYS runs!
grantAdminAccess(data.userId); // 💥 Security bug
}
✅ Fixed
const formData = {
userId: parseInt(form.userId.value, 10),
isAdmin: form.isAdmin.checked, // boolean from checkbox
retryCount: Number(form.retryCount.value)
};
Or validate and coerce at the boundary with zod:
import { z } from 'zod';
const UserSchema = z.object({
userId: z.coerce.number().int().positive(),
isAdmin: z.boolean(),
retryCount: z.coerce.number().int().min(0).max(10)
});
const parsed = UserSchema.parse(rawFormData);
// Throws a detailed error if types don't match after coercion
💡 Pro tip: The
"false"string bug is particularly dangerous. In JavaScript, every non-empty string is truthy —if ("false")evaluates totrue. Always use strict boolean types when the value will be used in a conditional.
Error #8: Circular References When Serializing Objects
What's happening
A circular reference is when an object contains a reference back to itself (directly or through a chain). JSON.stringify() throws immediately when it encounters one.
The error you'll see
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'parent' -> object with constructor 'Object'
--- property 'child' closes the circle
How it happens in the wild
const order = {
id: "ORD-9821",
customer: { name: "Alex Mensah" }
};
order.self = order; // Circular reference!
JSON.stringify(order); // 💥 TypeError
DOM nodes, Express req/res objects, and Sequelize/Mongoose model instances are full of these.
✅ Fix option 1: Use a replacer to skip circular refs
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
});
}
console.log(safeStringify(order));
// {"id":"ORD-9821","customer":{...},"self":"[Circular]"}
✅ Fix option 2: Use the flatted library for full round-trip support
import { stringify, parse } from 'flatted';
const json = stringify(order); // Handles circular refs
const restored = parse(json); // Restores the circular structure
✅ Fix option 3: Serialize only what you need
// Instead of serializing the whole ORM instance:
const orderForAPI = {
id: order.id,
customerName: order.customer.name,
items: order.items.map(i => ({ sku: i.sku, qty: i.quantity }))
};
JSON.stringify(orderForAPI); // Clean, no circular refs
💡 Pro tip: This error is extremely common when logging Express request objects in error handlers. Never do
logger.info(JSON.stringify(req)). Logreq.method,req.url,req.headers, andreq.bodyindividually instead.
Error #9: Date Objects — The Serialization Trap
What's happening
JSON.stringify() converts Date objects to ISO 8601 strings automatically. But JSON.parse() does not convert them back. You get a plain string — silently, every time.
The error you'll see
No error at parse time. But later:
const event = JSON.parse(JSON.stringify({ createdAt: new Date() }));
console.log(typeof event.createdAt); // "string" — not a Date!
console.log(event.createdAt.getTime()); // TypeError: event.createdAt.getTime is not a function
❌ Wrong pattern
const meeting = {
title: "Sprint Planning",
scheduledAt: new Date("2025-06-10T09:00:00Z"),
duration: 60
};
const stored = JSON.stringify(meeting);
const restored = JSON.parse(stored);
// This silently fails — string vs number comparison is NaN
if (restored.scheduledAt > Date.now()) {
sendReminder(); // Never called
}
✅ Fixed
const restored = JSON.parse(stored);
// Option 1: Parse manually
restored.scheduledAt = new Date(restored.scheduledAt);
// Option 2: Use a reviver function
const restoredSafe = JSON.parse(stored, (key, value) => {
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
return new Date(value);
}
return value;
});
// Option 3: Use date-fns
import { parseISO } from 'date-fns';
const scheduledAt = parseISO(restored.scheduledAt);
💡 Pro tip: The cleanest architecture is to never store
Dateobjects in state that crosses a serialization boundary. Use ISO strings throughout your app and only convert toDatewhen you actually need.getTime(),.toLocaleString(), etc.
Error #10: Large Integers and Precision Loss
What's happening
JavaScript uses 64-bit floating point (IEEE 754) for all numbers. The maximum safe integer is 2⁵³ − 1 (Number.MAX_SAFE_INTEGER = 9007199254740991). Any integer larger than this cannot be represented exactly — and JSON parsing silently rounds it.
The error you'll see
No error. Just silently wrong data:
const response = `{"id": 9007199254740993, "userId": 92233720368547758}`;
const parsed = JSON.parse(response);
console.log(parsed.id); // 9007199254740992 — off by 1!
console.log(parsed.userId); // 92233720368547760 — completely wrong!
This is a real problem with PostgreSQL 64-bit IDs, Twitter/X Snowflake IDs, and distributed systems.
✅ Fix option 1: Use strings for large IDs
The Twitter/X API famously added an id_str field alongside id for this exact reason:
{
"id": 9007199254740993,
"id_str": "9007199254740993",
"text": "JSON precision bugs are the worst"
}
Always use id_str in client code.
✅ Fix option 2: Pre-process with a regex before parsing
function parseBigInts(jsonString) {
return JSON.parse(
jsonString.replace(
/(":\s*)(-?\d{17,})/g,
(_, prefix, num) => `${prefix}"${num}"`
)
);
}
const safe = parseBigInts(`{"snowflakeId": 927349827349827349}`);
console.log(safe.snowflakeId); // "927349827349827349" — exact string
✅ Fix option 3: Use the json-bigint library
import JSONBig from 'json-bigint';
const parsed = JSONBig.parse(`{"id": 9007199254740993}`);
console.log(parsed.id); // 9007199254740993n — a real BigInt
💡 Pro tip: This bug only manifests for IDs large enough to exceed
MAX_SAFE_INTEGER. Your test data (IDs like1,42,1001) will never trigger it. It only hits in production. Always verify your ID generation strategy before assuming small numbers.
Debugging Toolkit
When you're staring at a broken JSON payload, here's your workflow:
1. Validate in Node
node -e "JSON.parse(require('fs').readFileSync('config.json', 'utf8'))" && echo "Valid"
2. Pretty-print with Python (no dependencies)
echo '{"a":1,"b":[1,2,3]}' | python3 -m json.tool
3. Validate + query with jq
cat data.json | jq .
4. Validate in your CI/CD pipeline
# In your package.json scripts:
"validate:config": "node -e \"JSON.parse(require('fs').readFileSync('./config.json'))\"",
Quick Reference Cheat Sheet
JSON CHEAT SHEET
══════════════════════════════════════════════════════════════
SYNTAX RULES VALID TYPES
───────────────────────────── ───────────────────────
✅ Double quotes only String: "hello"
✅ No trailing commas Number: 42, 3.14
✅ No comments Boolean: true, false
✅ Balanced brackets/braces Null: null
✅ Keys must be strings Array: [1, 2, 3]
Object: {"k": "v"}
INVALID VALUES (use null instead)
─────────────────────────────────
❌ undefined ❌ NaN ❌ Infinity ❌ Functions
ESCAPE SEQUENCES IN STRINGS
─────────────────────────────────
\" → " \\ → \ \n → newline
\t → tab \r → carriage return
\uXXXX → Unicode character
SPECIAL CASES TO WATCH FOR
─────────────────────────────────
• Dates → Store as ISO strings; parse explicitly
• Big ints → Use strings for IDs > 9007199254740991
• Circular refs → Use replacer or flatted library
• Type coercion → "false" ≠ false; "3" ≠ 3
══════════════════════════════════════════════════════════════
Wrapping Up
JSON errors almost always fall into one of two camps:
Syntax errors — trailing commas, wrong quotes, missing brackets. These are the easiest to fix. A good linter catches them before you run a single line of code. Add JSON validation to your editor, run
JSON.parse()insidetry/catcheverywhere you accept external data.Semantic traps — Dates that lose their type, integers that lose precision,
undefinedthat silently vanishes. These are trickier because they don't throw errors — they silently corrupt your data. The fix is validation at the boundary usingzod,ajv, or even a careful manual check before you trust the output ofJSON.parse().
The patterns that burn developers the most are #3 (NaN/undefined disappearing silently), #9 (Dates becoming strings), and #10 (large integers rounding). If you add nothing else to your workflow, add a JSON schema validator at every API boundary.
Which of these has bitten you hardest? My personal nemesis is #9 — every single time. Drop the error number in the comments.
Top comments (0)