How to validate JSON

Syntactic vs structural validation, with recipes for TypeScript (Zod), Python (Pydantic), Go, and language-agnostic JSON Schema.

6 min read

"Valid JSON" means two very different things depending on who's asking. Sometimes the question is syntactic — does this string parse? Sometimes it's structural — does this object have the fields my code expects? Getting these confused is one of the most common sources of production bugs. Here's how to handle both, in whichever language you're using.

Quick check: paste your JSON into the JSON formatter to validate syntax and get a pretty-printed version instantly.

Is valid JSON? The parseability test

The cheapest check. If JSON.parse (or your language's equivalent) throws, the string isn't JSON. That's it. This is the first answer to "is valid json", "json validation", and "js validate json".

// JavaScript / TypeScript
function isJsonSyntax(s: string): boolean {
  try { JSON.parse(s); return true; }
  catch { return false; }
}

For a quick eyeball check while debugging, paste it into the JSON formatter — you'll get a pretty-printed version if it parses and a clear error message pointing at the offending character if it doesn't.

JS validate JSON with a schema

Parsing says nothing about whether email is a string or age is a positive integer. For structural validation you need a schema. Below are the idiomatic approaches in JavaScript/TypeScript, Python, Go, and with a language-agnostic JSON Schema.

TypeScript / JavaScript — Zod

import { z } from "zod";

const User = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  roles: z.array(z.enum(["admin", "user"])).min(1),
});

// Throws on invalid input, returns typed value on success.
const user = User.parse(await res.json());

// Non-throwing variant returns a discriminated union.
const result = User.safeParse(data);
if (!result.success) console.error(result.error.flatten());

Zod is the modern default. Valibot and ArkType are lighter-weight alternatives with similar APIs. All three give you a runtime validator and a TypeScript type from a single definition.

Language-agnostic — JSON Schema + Ajv

import Ajv from "ajv";

const schema = {
  type: "object",
  properties: {
    id: { type: "string", format: "uuid" },
    email: { type: "string", format: "email" },
    age: { type: "integer", minimum: 0, maximum: 150 },
  },
  required: ["id", "email"],
  additionalProperties: false,
};

const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(schema);
if (!validate(data)) console.error(validate.errors);

Use JSON Schema when the schema needs to be portable across languages — an OpenAPI spec, a shared config format, or a document validated by both a JS frontend and a Python backend. Bootstrap one from a sample with the JSON to JSON Schema tool.

Python — Pydantic

from pydantic import BaseModel, EmailStr, Field
from uuid import UUID

class User(BaseModel):
    id: UUID
    email: EmailStr
    age: int = Field(ge=0, le=150)
    roles: list[str]

user = User.model_validate(payload)  # raises ValidationError on bad data

Go — encoding/json + validator

type User struct {
    ID    string `json:"id" validate:"required,uuid4"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"gte=0,lte=150"`
}

var u User
if err := json.Unmarshal(body, &u); err != nil { /* syntax error */ }
if err := validate.Struct(u); err != nil { /* semantic error */ }

Skeleton generated by the JSON to Go struct tool; add the validate tags by hand.

Where to validate — at the boundary, once

Validate exactly once, as early as possible: at the HTTP handler, queue consumer, or config loader. After that boundary, trust the parsed type. Repeating validation in every function that touches the data is noise and creates opportunities for drift.

Errors your users can act on

  • Return the path to the offending field (body.user.email), not just "invalid".
  • Return the expected shape — "expected string, got number".
  • Report all errors on one pass (allErrors: true in Ajv, abortEarly: false in Yup) so users don't fix one and hit the next.
  • Never leak the raw stack trace to clients — log it, return a stable error code.

Common pitfalls

  • Trusting Content-Type: application/json. Anyone can send that header with any payload. Always parse.
  • Casting instead of parsing. data as User in TypeScript is not validation — it's a promise to the compiler that you already validated. If you didn't, the compiler can't help.
  • Forgetting additionalProperties: false. Without it, JSON Schema silently accepts extra fields. Usually fine, sometimes a security issue (mass assignment).
  • Validating query strings with a JSON validator. Query strings are always strings — coerce first, or use a validator with coercion built in.

Frequently asked questions

How do I check if a string is valid JSON?
Try parsing it. In JavaScript, JSON.parse(str) throws if the string is invalid. Use the JSON formatter for a visual check with line numbers.
What is the difference between JSON validation and JSON parsing?
Parsing checks whether the string is valid JSON syntax. Validation checks whether the parsed object matches an expected shape or schema.
How do I validate JSON in JavaScript?
For syntax, use JSON.parse. For structure, use a schema library like Zod, Ajv, or Yup.
Can I validate JSON online?
Yes — paste it into the JSON formatter to confirm it parses, or use the JSON to JSON Schema tool to generate a schema you can validate against.

TL;DR

  1. Parse the string. If it throws, it's not JSON.
  2. Run it through a schema (Zod, Pydantic, Ajv). If it fails, reject with a useful error.
  3. Trust the resulting typed value inside your app.

Try the tools

Related reading