JSON Validation: A Complete Developer's Guide
JSON is simple enough to read by eye, but its syntax rules are strict enough that a single misplaced comma can break an entire pipeline. This guide covers JSON syntax rules, how to validate JSON in different languages, JSON Schema for structural validation, JSONPath for querying, pretty-printing, minifying, diffing, and every common error you'll encounter and how to fix it.
JSON (JavaScript Object Notation) is defined by RFC 8259. The specification is intentionally small. Valid JSON has exactly six data types:JSON Syntax Rules
| Type | Example | Notes |
|---|---|---|
| String | "hello world" | Double quotes only. Never single quotes. |
| Number | 42, 3.14, -17, 1.5e10 | No quotes. No leading zeros (except "0.x"). No NaN or Infinity. |
| Boolean | true, false | Lowercase only. True and False are invalid. |
| Null | null | Lowercase only. |
| Array | [1, "two", true, null] | Ordered. Can mix types. No trailing comma. |
| Object | {"key": "value"} | Keys must be double-quoted strings. No trailing comma. |
The Rules That Trip People Up
Double quotes are mandatory. This is different from JavaScript where both 'text' and "text" work. In JSON, 'text' is a syntax error.
No trailing commas. The last element in an array and the last property in an object cannot have a trailing comma. JavaScript added trailing comma support in ES5, but JSON never did.
No comments. RFC 8259 explicitly excludes comments. They were removed from an early draft. If you need comments in config files, use JSON5 (superset with comments), JSONC (JSON with Comments, used by VS Code), or YAML.
No undefined. JavaScript has undefined; JSON doesn't. Use null for absent values.
No JavaScript expressions. No functions, no new Date(), no Infinity, no NaN. JSON is a data format, not JavaScript.
Object keys must be strings. In JavaScript, {1: "one"} is valid (key gets coerced to string "1"). In JSON, keys must be quoted strings explicitly: {"1": "one"}.
Valid JSON Example
{
"name": "Jane Smith",
"age": 29,
"active": true,
"email": null,
"scores": [98, 87, 92],
"address": {
"city": "Portland",
"state": "OR",
"zip": "97201"
}
}String Escape Sequences
Inside JSON strings, these are the only valid escape sequences:
" Double quote (needed to embed " inside a string)
\ Backslash
/ Forward slash (optional, for compatibility)
\b Backspace
\f Form feed
\n Newline
\r Carriage return
\t Tab
\uXXXX Unicode character (4 hex digits)Literal newlines inside strings are not allowed — they must be escaped as \n. This is a frequent source of confusion for people coming from YAML or TOML.
Common Errors and Fixes
Single Quotes Instead of Double Quotes
// Invalid
{'name': 'John', 'active': true}// Valid
{"name": "John", "active": true}This is the most common error from people who write a lot of JavaScript. Train your fingers to use double quotes for JSON.
Trailing Commas
// Invalid — comma after last item
{
"name": "John",
"age": 30,
}
// Also invalid in arrays
[1, 2, 3,]
// Valid
{
"name": "John",
"age": 30
}Text editors with JSON awareness (VS Code, WebStorm) will flag trailing commas. If you frequently write JSON by hand, enable JSON linting in your editor.
Unquoted Keys
// Invalid
{name: "John", age: 30}// Valid
{"name": "John", "age": 30}JavaScript-Specific Values
// Invalid — JavaScript constructs not allowed in JSON
{
"fn": function() { return 42; },
"value": undefined,
"timestamp": new Date(),
"infinity": Infinity,
"nan": NaN
}// Valid — use JSON-compatible representations
{
"fn": null,
"value": null,
"timestamp": "2024-03-15T10:30:00Z",
"infinity": null,
"nan": null
}Unescaped Special Characters
// Invalid — unescaped double quote inside string
{"message": "She said "Hello""}// Valid
{"message": "She said "Hello""}Literal Newlines in Strings
// Invalid — literal newline inside string value
{
"poem": "Roses are red
Violets are blue"
}// Valid
{"poem": "Roses are red\nViolets are blue"}Comments
// Invalid — JSON doesn't support comments
{
// Database configuration
"host": "localhost",
"port": 5432 /* PostgreSQL default */
}// Valid — use _comment convention as workaround
{
"_comment": "Database configuration",
"host": "localhost",
"port": 5432
}Or switch to JSON5 / JSONC for config files where comments are important.
Number Edge Cases
// Invalid numbers
{"a": 01} // Leading zero (except 0.x is fine)
{"b": .5} // Missing leading digit (must be 0.5)
{"c": 1.} // Trailing dot
{"d": Infinity} // Not a valid JSON number
{"e": NaN} // Not a valid JSON number// Valid
{"a": 1, "b": 0.5, "c": 1.0, "d": null, "e": null}Validating in JavaScript, Python, and CLI
JavaScript / Node.js
JSON.parse() throws a SyntaxError on invalid JSON:
function validateJSON(str) {
try {
const parsed = JSON.parse(str);
return { valid: true, data: parsed };
} catch (e) {
return {
valid: false,
error: e.message
// e.message includes position info: "Unexpected token , in JSON at position 42"
};
}
}
// Usage
const result = validateJSON('{"name": "John", "age": 30}');
console.log(result.valid); // true
const bad = validateJSON('{"name": "John", "age": 30,}');
console.log(bad.valid); // false
console.log(bad.error); // "Unexpected token } in JSON at position 27"
// Validating a JSON file in Node.js
import fs from 'fs';
try {
const data = JSON.parse(fs.readFileSync('data.json', 'utf-8'));
console.log('Valid JSON');
} catch (e) {
console.error('Invalid JSON:', e.message);
process.exit(1);
}
Python
import json
def validate_json(json_string: str) -> tuple[bool, str]:
try:
json.loads(json_string)
return True, ""
except json.JSONDecodeError as e:
return False, f"Line {e.lineno}, col {e.colno}: {e.msg}"
Usage
valid, err = validate_json('{"name": "John"}')
print(valid) # True
valid, err = validate_json("{'name': 'John'}")
print(valid, err) # False, "Line 1, col 2: Expecting property name enclosed in double quotes"
Validating a file
import sys
try:
with open('data.json') as f:
json.load(f)
print("Valid JSON")
except json.JSONDecodeError as e:
print(f"Invalid: {e}", file=sys.stderr)
sys.exit(1)
CLI with jq
jq is the Swiss Army knife for JSON on the command line:
# Validate (exit code 0 = valid, non-zero = invalid)
jq . data.json > /dev/null && echo "Valid" || echo "Invalid"
Or with explicit exit code check
if jq -e . data.json > /dev/null 2>&1; then
echo "Valid JSON"
else
echo "Invalid JSON"
fi
Pretty-print while validating
jq . data.json
Compact output
jq -c . data.json
Validate and show error details
jq . invalid.json 2>&1 # jq prints errors to stderr with position info
Validate multiple files
for f in *.json; do
if jq -e . "$f" > /dev/null 2>&1; then
echo "OK: $f"
else
echo "FAIL: $f"
fi
done
The JSON Validator and JSON Formatter on ToolsDock handle this without any command-line setup — paste or upload, get results instantly.
JSON syntax validation tells you whether a document is parseable. JSON Schema validation tells you whether it has the right structure and data types. They're complementary — a JSON document can be syntactically perfect but structurally wrong for your application.JSON Schema
What JSON Schema Validates
- Data types (
type: "string","number","boolean","array","object","null") - Required properties and allowed additional properties
- String patterns (
pattern), min/max length - Number ranges (
minimum,maximum,multipleOf) - Array length (
minItems,maxItems) and item types - Allowed values (
enum) - Complex conditions (
if/then/else,oneOf,anyOf,allOf)
A Real JSON Schema Example
Suppose you have an API that accepts user profile updates. The schema defines exactly what's valid:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "User Profile Update",
"type": "object",
"required": ["id", "email"],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
"minimum": 1,
"description": "User ID — must be a positive integer"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 255
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"age": {
"type": "integer",
"minimum": 18,
"maximum": 120
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"]
},
"tags": {
"type": "array",
"items": {
"type": "string",
"maxLength": 50
},
"uniqueItems": true,
"maxItems": 10
},
"metadata": {
"type": "object",
"description": "Arbitrary key-value metadata"
}
}
}This schema enforces: id and email are required; age must be 18-120 if present; role must be one of the three specified values; tags must be unique strings. No extra properties are allowed.
Validating Against a Schema in JavaScript
The two most popular JSON Schema validators for JavaScript are Ajv (fastest) and others:
import Ajv from 'ajv';
import addFormats from 'ajv-formats'; // for "email", "date-time", etc.
const ajv = new Ajv({ allErrors: true }); // allErrors: show all errors, not just first
addFormats(ajv);
const schema = {
type: 'object',
required: ['id', 'email'],
properties: {
id: { type: 'integer', minimum: 1 },
email: { type: 'string', format: 'email' },
role: { type: 'string', enum: ['admin', 'editor', 'viewer'] }
},
additionalProperties: false
};
const validate = ajv.compile(schema);
const data = { id: 42, email: 'user@example.com', role: 'admin' };
const valid = validate(data);
if (!valid) {
console.log(validate.errors);
// [{ instancePath: '/email', message: 'must match format "email"', ... }]
} else {
console.log('Valid!');
}
Validating Against a Schema in Python
<code">from jsonschema import validate, ValidationError, SchemaError import jsonschema = { "type": "object", "required": ["id", "email"], "properties": { "id": {"type": "integer", "minimum": 1}, "email": {"type": "string", "format": "email"}, "role": {"type": "string", "enum": ["admin", "editor", "viewer"]} }, "additionalProperties": False }
data = {"id": 42, "email": "user@example.com", "role": "admin"}
try: validate(instance=data, schema=schema) print("Valid") except ValidationError as e: print(f"Invalid: {e.message}") print(f"Path: {' -> '.join(str(p) for p in e.path)}") except SchemaError as e: print(f"Schema itself is invalid: {e.message}")
Schema Composition
JSON Schema lets you compose schemas to avoid repetition and express complex rules:
<code">{
"oneOf": [
{ "required": ["creditCard"] },
{ "required": ["bankAccount"] }
],
"properties": {
"creditCard": {
"type": "object",
"required": ["number", "expiry", "cvv"],
"properties": {
"number": { "type": "string", "pattern": "^\d{16}$" },
"expiry": { "type": "string", "pattern": "^\d{2}/\d{2}$" },
"cvv": { "type": "string", "pattern": "^\d{3,4}$" }
}
},
"bankAccount": {
"type": "object",
"required": ["routing", "account"],
"properties": {
"routing": { "type": "string", "pattern": "^\d{9}$" },
"account": { "type": "string" }
}
}
}
}oneOf requires exactly one of the sub-schemas to match. anyOf allows one or more. allOf requires all. if/then/else enables conditional validation — validate differently based on a field's value.
JSONPath is to JSON what XPath is to XML — a query language for extracting values from JSON documents. It's useful for validation (does this path exist?), transformation (extract specific fields), and testing (assert expected values).JSON Path Queries
JSONPath Syntax
Given this JSON:
{
"store": {
"books": [
{ "title": "Clean Code", "price": 29.99, "inStock": true },
{ "title": "The Pragmatic Programmer", "price": 39.99, "inStock": false },
{ "title": "SICP", "price": 0.00, "inStock": true }
],
"currency": "USD"
}
}
$.store.currency → "USD"
$.store.books[0].title → "Clean Code"
$.store.books[*].title → ["Clean Code", "The Pragmatic Programmer", "SICP"]
$.store.books[?(@.price < 30)] → books where price < 30
$.store.books[?(@.inStock)] → books where inStock is true
$..title → All titles (recursive descent)
$.store.books[-1:] → Last book in array
JSONPath in JavaScript
<code">// Using jsonpath-plus library
import { JSONPath } from 'jsonpath-plus';
const data = { store: { books: [...] } };
// Get all in-stock book titles
const titles = JSONPath({ path: '$.store.books[?(@.inStock)].title', json: data });
// ["Clean Code", "SICP"]
// Check existence
const hasBooks = JSONPath({ path: '$.store.books', json: data }).length > 0;
// Using native JSON + array methods (often simpler for simple queries)
const inStockBooks = data.store.books.filter(b => b.inStock);
const cheapBooks = data.store.books.filter(b => b.price < 30);
jq for JSONPath-style Queries
jq has its own filter syntax that's more powerful than standard JSONPath:
<code"># Get all book titles jq '.store.books[].title' data.jsonFilter in-stock books and get their titles
jq '[.store.books[] | select(.inStock) | .title]' data.json
Get books under $30
jq '[.store.books[] | select(.price < 30)]' data.json
Transform: create a summary object
jq '{total: (.store.books | length), inStock: [.store.books[] | select(.inStock)] | length}' data.json
Extract values and format as CSV
jq -r '.store.books[] | [.title, .price] | @csv' data.json
Pretty-Printing and Minifying
Why Format Matters
Minified JSON — all on one line — is fine for machines but hard for humans to debug. Pretty-printed JSON with consistent indentation makes structure immediately visible. The tradeoff: whitespace adds bytes (usually 10-30% overhead). For API responses over the wire, minify. For log files and config files humans read, pretty-print.
Pretty-Printing
# JavaScript — built in
JSON.stringify(data, null, 2); // 2-space indent
JSON.stringify(data, null, 4); // 4-space indent
JSON.stringify(data, null, '\t'); // Tab indent
Python — built in
import json
json.dumps(data, indent=2)
json.dumps(data, indent=2, sort_keys=True) # Also sort keys alphabetically
jq
jq . data.json # Default pretty-print (2 spaces)
jq --tab . data.json # Tab indent
Node.js one-liner
node -e "console.log(JSON.stringify(require('./data.json'), null, 2))"
Minifying
<code"># JavaScript JSON.stringify(data); // No whitespace by defaultPython
json.dumps(data, separators=(',', ':')) # Remove all whitespace
jq
jq -c . data.json # Compact (minified) output
Node.js one-liner
node -e "process.stdout.write(JSON.stringify(require('./data.json')))"
The JSON Formatter and JSON Validator handle both operations without any setup.
Sorting Keys
JSON objects are technically unordered (the spec says so), but sorting keys alphabetically makes diffs cleaner and makes it easier to check for the presence of a field:
<code"># Python json.dumps(data, indent=2, sort_keys=True)jq
jq -S . data.json # -S sorts keys
JavaScript — no built-in key sort; use a replacer
function sortedStringify(obj) { return JSON.stringify(obj, Object.keys(obj).sort(), 2); } // Note: this only sorts top-level keys; for deep sorting, recurse
Comparing two JSON documents with a text diff often produces noisy results — a key order change creates a huge diff even if the actual data is identical. A semantic JSON diff understands that Diffing JSON
{"a":1,"b":2} and {"b":2,"a":1} are equivalent.
CLI Diff with jq
<code"># Sort both files before diff to normalize key order diff <(jq -S . file1.json) <(jq -S . file2.json)Or use json-diff (npm package)
npx json-diff file1.json file2.json
Show only changed values
npx json-diff --json file1.json file2.json | jq .
Diff in JavaScript
<code"># Using deep-diff library
import { diff } from 'deep-diff';
const obj1 = { name: "Alice", age: 30, role: "admin" };
const obj2 = { name: "Alice", age: 31, role: "editor" };
const differences = diff(obj1, obj2);
// [
// { kind: 'E', path: ['age'], lhs: 30, rhs: 31 },
// { kind: 'E', path: ['role'], lhs: 'admin', rhs: 'editor' }
// ]
// kind: 'E' = edited, 'N' = new, 'D' = deleted, 'A' = array change
The JSON Diff tool at ToolsDock shows a visual side-by-side diff with added, removed, and changed values highlighted.
These terms get mixed up. They're different:Linting vs Validation
| JSON Syntax Validation | JSON Schema Validation | JSON Linting | |
|---|---|---|---|
| Checks | Is it parseable? | Does it match the schema? | Style and conventions |
| Catches | Missing commas, wrong quotes | Wrong types, missing fields | Inconsistent formatting, key order |
| Tools | JSON.parse, jq, json-validator | Ajv, jsonschema | jsonlint, ESLint |
| When to use | Always — first step | For APIs and structured data | In development, CI |
A typical pipeline: syntax validate first, schema validate second, lint for style in development. All three serve different purposes.
ESLint for JSON
<code"># Install ESLint JSON plugin npm install --save-dev eslint-plugin-json.eslintrc.json
{ "plugins": ["json"], "rules": { "json/*": ["error"] } }
Lint JSON files
npx eslint --ext .json .
Best Practices
API Design
- Always set
Content-Type: application/json; charset=utf-8 - Use ISO 8601 for dates:
"2024-03-15T10:30:00Z"— not timestamps, notnew Date() - Be consistent with naming: pick camelCase or snake_case and stick to it across all endpoints
- Use
nullfor absent values — don't omit fields sometimes and include them asnullother times - Don't use numbers as IDs if they might exceed 2^53 (JavaScript's safe integer limit). Use strings for large IDs.
- Version your schema — add a
versionfield when the format might change
Configuration Files
- Use JSONC (with comments) for human-authored config files when your tool supports it
- Consider YAML or TOML for config files that humans write frequently — they're more writable and allow comments
- Validate configuration against a JSON Schema on application startup — fail fast with clear error messages rather than cryptic runtime errors
- Use environment-specific files (
config.dev.json,config.prod.json) with a base that's merged
Working with Large JSON Files
- Streaming parsers (like
stream-jsonin Node.js) avoid loading the entire file into memory - NDJSON (Newline-Delimited JSON) — one JSON object per line — is much more efficient for large datasets and logs
- For files over a few MB, consider binary formats like MessagePack or CBOR for storage and transmission
Tools
JSON Validator
Check JSON syntax with detailed error messages pointing to the exact problem location.
Validate JSONJSON Formatter
Beautify minified JSON with configurable indentation. Also minifies formatted JSON.
Format JSONJSON Schema Validator
Validate JSON against a JSON Schema — check structure, types, and required fields.
Schema ValidateQuick Reference
Valid JSON
"text"true / falsenull{"key": "value"}" \ \nInvalid JSON
'text'[1, 2, 3,]// or /* */undefined{key: "value"}function() {}