BytePane

How to Validate JSON: Methods, Tools & Best Practices

JSON16 min read

The API That Accepted Invalid JSON for Six Months

A payments startup noticed its webhook ingestion service had silently dropped 0.3% of events since a refactor six months earlier. The culprit: a third-party partner had started sending JSON with trailing commas — valid JavaScript syntax, invalid JSON per RFC 8259. The service's original JSON.parse() call threw SyntaxError, the error handler swallowed it silently, and the events were lost without any alerting. Six months of 0.3% data loss. The fix was three lines of code. The detection took six months because there was no validation at the boundary.

JSON validation failures are common. According to research published in Scientific Reports (2026) on schema validation frameworks for JSON databases, the increasing use of schemaless data systems has made reliable JSON validation one of the most critical — and most neglected — quality controls in modern software. A survey by jsonconsole.com found that developers waste an average of 2.3 hours per week on JSON-related tasks that could be automated with proper validation tooling.

This guide covers the full validation stack: syntax validation to catch parse errors, JSON Schema to enforce structure, runtime validation libraries in JavaScript and Python, and CI/CD integration to catch bad JSON before it reaches production.

Key Takeaways

  • JSON validation has two distinct layers: syntax validation (is it parseable JSON?) and schema validation (does the structure match what you expect?). Syntax validation catches parse errors; schema validation catches semantic errors.
  • AJV (145M weekly npm downloads) is the performance choice for JavaScript: it compiles JSON Schemas into JIT-optimized validation functions, making repeated validation 10–50× faster than interpreted validators.
  • JSON has no comment syntax// comments and trailing commas are not valid JSON. If your config files need comments, use YAML or JSONC (JSON with Comments, supported by TypeScript's tsconfig.json).
  • Always validate JSON at system boundaries: incoming API requests, webhook payloads, user-uploaded files, external service responses. Internal JSON from your own code does not need runtime validation if you control the serializer.
  • Zod infers TypeScript types from validation schemas — write the schema once, get both runtime validation and compile-time types. No JSON Schema required.

Layer 1: Syntax Validation — Is It Valid JSON?

JSON syntax is defined in RFC 8259 (published December 2017), which superseded RFC 7159 and RFC 4627. The specification is deliberately minimal: JSON consists of six types — string, number, boolean, null, array, and object. RFC 8259 is strict where earlier RFCs were ambiguous, particularly around encoding (UTF-8 only, no BOM) and duplicate keys (generators must not produce them; parsers are not required to reject them).

The Six Most Common JSON Syntax Errors

Trailing commas
✗ Invalid
{"name": "Alice", "age": 30,}
✓ Valid
{"name": "Alice", "age": 30}

Valid in JavaScript and Python dictionaries, strictly forbidden in JSON. Many code editors and transpilers silently accept trailing commas; JSON.parse() does not.

Single-quoted strings
✗ Invalid
{'name': 'Alice'}
✓ Valid
{"name": "Alice"}

JSON requires double quotes for all strings — both keys and values. Single quotes produce SyntaxError in every compliant parser.

Unquoted property names
✗ Invalid
{name: "Alice", age: 30}
✓ Valid
{"name": "Alice", "age": 30}

JavaScript object literals allow unquoted keys; JSON does not. This is the most common mistake for developers coming from JavaScript.

Comments
✗ Invalid
{"port": 8080 // default port }
✓ Valid
{"port": 8080}

JSON has no comment syntax — no // or /* */. Use JSONC for config files that need comments (VS Code settings.json, tsconfig.json are JSONC, not JSON).

undefined, NaN, or Infinity values
✗ Invalid
{"result": undefined, "score": NaN}
✓ Valid
{"result": null, "score": 0}

These are JavaScript-specific values. JSON has null but not undefined, NaN, or Infinity. JSON.stringify() drops undefined values and converts NaN/Infinity to null.

Unescaped control characters in strings
✗ Invalid
{"description": "line 1 line 2"}
✓ Valid
{"description": "line 1\nline 2"}

Literal newlines, tabs, and other control characters (U+0000 through U+001F) must be escaped inside JSON strings: \n, \t, \r, \u0000–\u001F.

Syntax Validation in Code

Syntax validation with precise error location
// JavaScript — JSON.parse with error location
function validateJsonSyntax(text: string): { valid: boolean; error?: string } {
  try {
    JSON.parse(text)
    return { valid: true }
  } catch (e) {
    // JSON.parse errors include position info in modern engines
    return {
      valid: false,
      error: (e as SyntaxError).message,
      // e.g. "Unexpected token ',' at position 42"
    }
  }
}

// Python — json.loads with line/column position
import json

def validate_json_syntax(text: str) -> tuple[bool, str | None]:
    try:
        json.loads(text)
        return True, None
    except json.JSONDecodeError as e:
        # JSONDecodeError includes line, col, and pos attributes
        return False, f"Line {e.lineno}, col {e.colno}: {e.msg}"

# CLI — jq for scripts
# jq . file.json > /dev/null && echo "Valid" || echo "Invalid"

# CLI — Python one-liner
# python3 -m json.tool file.json > /dev/null && echo "Valid"

For an instant online check, the BytePane JSON formatter validates syntax and shows errors with line numbers as you type — no server round-trip, runs in your browser.

Layer 2: JSON Schema Validation — Does It Have the Right Structure?

Syntax validation only confirms that a string is parseable JSON. Schema validation confirms that the parsed data has the structure your code expects: correct types for each field, required fields present, string patterns matched, numeric values in range. The standard vocabulary for this is JSON Schema, currently at draft 2020-12 (published October 2020 at json-schema.org).

JSON Schema: Core Vocabulary

A real-world JSON Schema for a REST API user object
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://api.example.com/schemas/user.json",
  "type": "object",
  "title": "User",
  "description": "User account object returned by /users/{id}",
  "required": ["id", "email", "createdAt", "role"],
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "description": "UUID v4 user identifier"
    },
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 254
    },
    "displayName": {
      "type": ["string", "null"],
      "minLength": 1,
      "maxLength": 100
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 datetime"
    },
    "role": {
      "type": "string",
      "enum": ["admin", "editor", "viewer"],
      "description": "Access control role"
    },
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 150
    },
    "tags": {
      "type": "array",
      "items": { "type": "string", "maxLength": 50 },
      "uniqueItems": true,
      "maxItems": 20
    },
    "address": {
      "type": "object",
      "required": ["country"],
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": {
          "type": "string",
          "pattern": "^[A-Z]{2}$",
          "description": "ISO 3166-1 alpha-2 country code"
        }
      }
    }
  }
}

JSON Schema Validation Keyword Reference

KeywordApplies ToDescriptionExample
typeAllValidates the JSON type"type": "string"
requiredObjectList of required keys"required": ["id", "email"]
propertiesObjectSchema per key"properties": { "age": {...} }
additionalPropertiesObjectAllow/schema for extra keys"additionalProperties": false
enumAllAllowed values list"enum": ["admin", "viewer"]
minimum / maximumNumberNumeric bounds (inclusive)"minimum": 0, "maximum": 100
minLength / maxLengthStringString length bounds"minLength": 1, "maxLength": 255
patternStringRegex the string must match"pattern": "^[A-Z]{2}$"
formatStringSemantic format (email, date-time)"format": "email"
itemsArraySchema for array elements"items": { "type": "string" }
uniqueItemsArrayAll elements must be distinct"uniqueItems": true
$refAllReference another schema"$ref": "#/$defs/Address"
if / then / elseAllConditional validationif role=admin then require adminLevel

AJV: The JavaScript Schema Validator That Compiles to Code

AJV (Another JSON Validator) has 145 million weekly npm downloads — it is the de facto standard for JSON Schema validation in Node.js. Its key differentiator: AJV compiles your JSON Schema into a JavaScript validation function at startup, rather than interpreting the schema on each validation call. The compiled function is JIT-optimized by V8, making it 10–50× faster than interpreted validators for repeated validation.

AJV — production setup with draft 2020-12
import Ajv, { type JSONSchemaType } from 'ajv'
import addFormats from 'ajv-formats'

const ajv = new Ajv({
  allErrors: true,       // collect all errors, not just the first
  strict: true,          // throw on unknown keywords (catches schema typos)
  coerceTypes: false,    // NEVER set to true in APIs — silently changes input data
})
addFormats(ajv)          // adds format: "email", "date-time", "uri", "uuid", etc.

// Define TypeScript interface and JSON Schema together
interface User {
  id: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
  age?: number
}

const userSchema: JSONSchemaType<User> = {
  type: 'object',
  properties: {
    id: { type: 'string', format: 'uuid' },
    email: { type: 'string', format: 'email' },
    role: { type: 'string', enum: ['admin', 'editor', 'viewer'] },
    age: { type: 'number', nullable: true, minimum: 0, maximum: 150 },
  },
  required: ['id', 'email', 'role'],
  additionalProperties: false,
}

// Compile once at startup — reuse the validate function for all requests
const validateUser = ajv.compile(userSchema)

// In your Express/Fastify route handler
function handleUserCreate(body: unknown): User {
  if (!validateUser(body)) {
    const errors = validateUser.errors!.map(e =>
      `${e.instancePath || '(root)'}: ${e.message}`
    )
    throw new ValidationError('Invalid user payload', errors)
  }
  // body is now typed as User — TypeScript knows the shape
  return body
}

// Example error output (allErrors: true):
// ["/email: must match format "email"",
//  "/role: must be equal to one of the allowed values",
//  "/id: must match format "uuid""]
AJV with $ref — reusable schema components
const ajv = new Ajv({ allErrors: true })
addFormats(ajv)

// Define reusable schema components
ajv.addSchema({
  $id: 'https://api.example.com/schemas/address.json',
  type: 'object',
  properties: {
    street: { type: 'string' },
    city: { type: 'string' },
    country: { type: 'string', pattern: '^[A-Z]{2}$' },
    postalCode: { type: 'string' },
  },
  required: ['city', 'country'],
})

// Reference in another schema
const orderSchema = {
  type: 'object',
  properties: {
    id: { type: 'string' },
    shippingAddress: { $ref: 'https://api.example.com/schemas/address.json' },
    billingAddress:  { $ref: 'https://api.example.com/schemas/address.json' },
  },
  required: ['id', 'shippingAddress'],
}

const validateOrder = ajv.compile(orderSchema)

Zod: TypeScript-First Schema Validation

Zod takes a different approach: instead of writing a JSON Schema object, you define your schema using a TypeScript-native API, and Zod infers the TypeScript type automatically. The ergonomics are excellent for TypeScript projects — you write the schema once and get both runtime validation and compile-time type checking.

Zod — schema definition + type inference
import { z } from 'zod'

// Define schema — also serves as the TypeScript type definition
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email().max(254),
  displayName: z.string().min(1).max(100).nullable().optional(),
  role: z.enum(['admin', 'editor', 'viewer']),
  age: z.number().int().min(0).max(150).optional(),
  createdAt: z.string().datetime(),
  tags: z.array(z.string().max(50)).max(20).default([]),
  address: z.object({
    street: z.string().optional(),
    city: z.string(),
    country: z.string().regex(/^[A-Z]{2}$/, 'Must be ISO 3166-1 alpha-2'),
  }).optional(),
})

// Infer the TypeScript type from the schema — no duplication
type User = z.infer<typeof UserSchema>
// type User = {
//   id: string;
//   email: string;
//   displayName?: string | null;
//   role: "admin" | "editor" | "viewer";
//   age?: number;
//   ...
// }

// Validation — throws ZodError with full path on failure
function validateAndParseUser(input: unknown): User {
  return UserSchema.parse(input)
  // Throws: ZodError with array of issues, each with path and message
}

// Safe parse — returns success/error result instead of throwing
function safeValidate(input: unknown) {
  const result = UserSchema.safeParse(input)
  if (!result.success) {
    const errors = result.error.issues.map(i =>
      `${i.path.join('.')}: ${i.message}`
    )
    return { valid: false, errors }
  }
  return { valid: true, data: result.data }
}

// Partial schemas for PATCH endpoints
const UserPatchSchema = UserSchema.partial()
// All fields become optional — correct for PATCH

// Pick subset for response shapes
const UserPublicSchema = UserSchema.pick({ id: true, displayName: true, role: true })
type UserPublic = z.infer<typeof UserPublicSchema>

Python: jsonschema and fastjsonschema

Python jsonschema — validation with detailed error reporting
import json
import jsonschema
from jsonschema import validate, ValidationError, Draft202012Validator

# Define the schema (JSON Schema draft 2020-12)
USER_SCHEMA = {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "required": ["id", "email", "role"],
    "additionalProperties": False,
    "properties": {
        "id": {"type": "string", "format": "uuid"},
        "email": {"type": "string", "format": "email", "maxLength": 254},
        "role": {"type": "string", "enum": ["admin", "editor", "viewer"]},
        "age": {"type": "integer", "minimum": 0, "maximum": 150},
    },
}

def validate_user(data: dict) -> list[str]:
    """Returns list of validation errors (empty list = valid)."""
    validator = Draft202012Validator(USER_SCHEMA)
    errors = sorted(validator.iter_errors(data), key=lambda e: e.path)
    return [
        f"{'.' .join(str(p) for p in e.absolute_path) or '(root)'}: {e.message}"
        for e in errors
    ]

# Usage
user_json = '{"id": "not-a-uuid", "email": "invalid", "role": "superuser"}'
data = json.loads(user_json)
errors = validate_user(data)
# [
#   "(root): Additional properties are not allowed",
#   "email: 'invalid' is not a 'email'",
#   "id: 'not-a-uuid' is not a 'uuid'",
#   "role: 'superuser' is not one of ['admin', 'editor', 'viewer']"
# ]

# ── fastjsonschema: 3-20x faster for high-frequency validation ──
import fastjsonschema

# Compile schema once at startup
validate_user_fast = fastjsonschema.compile(USER_SCHEMA)

# Validate (raises fastjsonschema.JsonSchemaValueException on failure)
try:
    validate_user_fast(data)
except fastjsonschema.JsonSchemaValueException as e:
    print(e.message)  # first error only (unlike jsonschema.iter_errors)

Validation Library Comparison

LibraryLanguageWeekly DownloadsJSON SchemaStrength
AJVJavaScript145M npmDraft 04–2020-12Fastest (JIT compiled)
ZodTypeScript12M npmNone (custom)TypeScript type inference
YupJavaScript7M npmNone (custom)Form validation, async rules
jsonschemaPython14M PyPIDraft 04–2020-12Full spec, iter_errors
fastjsonschemaPython5M PyPIDraft 04–073-20× faster than jsonschema
pydanticPython50M PyPINone (custom)FastAPI integration, data classes
Ajv (Go: gojsonschema)GoN/ADraft 04–07Standard choice for Go

Where to Validate: Boundaries, Not Everywhere

A common mistake is attempting to validate JSON everywhere in a codebase, including between internal services that you control. This produces noise and performance overhead without security benefit. The right principle: validate at trust boundaries.

Incoming HTTP request bodies
Untrusted external input — always validate. Reject early before business logic.
Webhook payloads from third parties
Partners change schemas without notice. Validate + log raw body before parsing.
User file uploads (JSON files)
Users can upload anything. Validate size, MIME type, AND JSON syntax.
External API responses you depend on
APIs change. Validate and alert on schema deviation before it breaks your code.
Internal service-to-service calls
If both services are your code, type safety at the serializer is enough. Runtime validation adds latency.
Database reads of your own data
You wrote the data; you own the schema. Trust your write-path validation.
Config files loaded at startup
Catch misconfiguration immediately — fail loud at startup, not silently at runtime.

Express Middleware: Validate Every Incoming Request

import express from 'express'
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
import type { JSONSchemaType } from 'ajv'

const ajv = new Ajv({ allErrors: true })
addFormats(ajv)

// Generic validation middleware factory
function validateBody<T>(schema: JSONSchemaType<T>) {
  const validate = ajv.compile(schema)

  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    if (!validate(req.body)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: validate.errors!.map(e => ({
          field: e.instancePath || '(root)',
          message: e.message,
        })),
      })
    }
    next()
  }
}

// Usage
interface CreateUserBody {
  email: string
  role: 'admin' | 'editor' | 'viewer'
}

const createUserSchema: JSONSchemaType<CreateUserBody> = {
  type: 'object',
  required: ['email', 'role'],
  properties: {
    email: { type: 'string', format: 'email' },
    role: { type: 'string', enum: ['admin', 'editor', 'viewer'] },
  },
  additionalProperties: false,
}

app.post('/users', validateBody(createUserSchema), (req, res) => {
  // req.body is validated — safe to use
  const { email, role } = req.body as CreateUserBody
  // ...
})

JSON Validation in CI/CD: Catch Bad JSON Before It Ships

GitHub Actions — validate all JSON config files on PR
# .github/workflows/validate-json.yml
name: Validate JSON

on:
  pull_request:
    paths: ['**/*.json', '!node_modules/**']

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Validate JSON syntax (all .json files)
        run: |
          find . -name "*.json"             -not -path "*/node_modules/*"             -not -path "*/.git/*"             -print0 | xargs -0 -I{} sh -c             'python3 -m json.tool {} > /dev/null && echo "OK: {}" || (echo "FAIL: {}" && exit 1)'

      - name: Validate package.json structure with AJV
        run: npx ajv validate -s .schemas/package-schema.json -d package.json

      - name: Validate API response fixtures
        run: |
          for fixture in tests/fixtures/*.json; do
            npx ajv validate -s schemas/api-response.json -d "$fixture"
          done

For a deeper dive on JSON Schema specifically — $ref, discriminated unions, if/then/else, and AJV JIT compilation — see the JSON Schema validation deep dive.

Frequently Asked Questions

How do I validate JSON syntax?

JavaScript: JSON.parse(text) — throws SyntaxError with position on failure. Python: json.loads(text) — throws json.JSONDecodeError with line/column. CLI: jq . file.json or python3 -m json.tool file.json. All return 0/non-zero exit codes usable in shell scripts. For browser-based validation, use the BytePane JSON formatter.

What is JSON Schema?

JSON Schema is an IETF vocabulary (json-schema.org, draft 2020-12) for annotating and validating JSON documents. It defines allowed types, required fields, value ranges, string patterns, and complex conditional rules. Validators like AJV (JavaScript, 145M weekly downloads) and jsonschema (Python) implement JSON Schema to validate documents at runtime against the schema.

What is AJV and why is it the most popular JSON validator?

AJV (Another JSON Validator) has 145M weekly npm downloads — the most used JSON Schema validator for JavaScript. It compiles schemas into optimized V8 functions at startup (JIT), making repeated validation 10-50× faster than interpreted validators. AJV supports JSON Schema drafts 04 through 2020-12 and adds format validators (email, uuid, date-time) via ajv-formats.

What is the difference between AJV and Zod?

AJV validates against a JSON Schema definition — a plain object following the JSON Schema standard. Zod defines schemas in TypeScript and infers TypeScript types from them automatically. Zod is ergonomic for TypeScript projects; AJV is better when you already have a JSON Schema spec or need to share schemas with non-JavaScript tools. Zod does not implement JSON Schema standard.

How do I validate JSON in Python?

Syntax: json.loads(text) — raises json.JSONDecodeError on failure. Schema: pip install jsonschema, then jsonschema.validate(instance=data, schema=schema). For high performance: pip install fastjsonschema — compiles schemas like AJV does. For FastAPI/Pydantic: define Pydantic models and FastAPI validates request bodies automatically.

What are the most common JSON syntax errors?

The six most common: (1) trailing commas (valid JavaScript, invalid JSON); (2) single-quoted strings instead of double-quoted; (3) unquoted property names; (4) comments (JSON has no comment syntax); (5) undefined/NaN/Infinity values (not JSON types); (6) literal newlines in strings (must be \n).

Validate JSON Instantly

Paste your JSON to check syntax, format, and spot errors with line numbers — entirely in your browser, no uploads.