How to validate JSON
Syntactic vs structural validation, with recipes for TypeScript (Zod), Python (Pydantic), Go, and language-agnostic JSON Schema.
"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 dataGo — 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: truein Ajv,abortEarly: falsein 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 Userin 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
- Parse the string. If it throws, it's not JSON.
- Run it through a schema (Zod, Pydantic, Ajv). If it fails, reject with a useful error.
- Trust the resulting typed value inside your app.