JSON Schema is the standard for describing and validating the structure of JSON data. It's used for API documentation, request/response validation, config file validation, and form validation. Here's a practical guide.
Why JSON Schema matters
Without schema validation:
``javascript
// Server receives this and crashes later
app.post('/users', (req, res) => {
const user = req.body;
// What if name is undefined? What if age is "thirty"?
db.create({ name: user.name.trim(), age: user.age });
});
`
With JSON Schema validation:
<code>javascript
<p>app.post('/users', validate(userSchema), (req, res) => {</p>
<p>// Only reaches here if data passes schema validation</p>
<p>db.create(req.body);</p>
<p>});</p>
</code>
For testing schemas interactively before implementing them in code, a JSON Schema validator shows validation errors with exact paths in the browser.
Core keywords reference
`json
{
"$schema": "http://json-schema.org/draft-07/schema#",</p>
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"pattern": "^[a-zA-Z ]+$" / Letters and spaces only /
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": ["admin", "user", "guest"] / Must be one of these /
},
"tags": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["name", "email"], / Required properties /
"additionalProperties": false / Reject unknown keys /
}
`
ajv (JavaScript — the most used validator)
<code>bash
<p>npm install ajv ajv-formats</p>
</code>
`javascript
import Ajv from 'ajv';
import addFormats from 'ajv-formats'; // For "email", "uri", "date", etc.
const ajv = new Ajv({
allErrors: true, // Report all errors (not just the first)
coerceTypes: false, // Don't coerce types (strict mode)
useDefaults: true // Fill in default values
});
addFormats(ajv);
const schema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 }
},
additionalProperties: false
};
const validate = ajv.compile(schema);
// Validate data
function validateData(data) {
const valid = validate(data);
if (!valid) {
return {
valid: false,
errors: validate.errors.map(e => ({
path: e.instancePath, // e.g. "/name" or "/address/city"
message: e.message, // e.g. "must have required property 'email'"
value: e.data // The actual value that failed
}))
};
}
return { valid: true };
}
// Usage
validateData({ name: 'Alice', email: '*Emails are not allowed*', age: 30 });
// → { valid: true }
validateData({ name: '', email: 'not-an-email' });
// → { valid: false, errors: [{path: '/name', message: 'must NOT have fewer than 1 characters'}, ...] }
`
Express.js middleware with ajv
`javascript
function validateBody(schema) {
const validate = ajv.compile(schema);
return (req, res, next) => {
if (!validate(req.body)) {
return res.status(400).json({
error: 'Validation failed',
details: validate.errors.map(e => ({
field: e.instancePath.replace(/^\//, '') || 'root',
message: e.message
}))
});
}
next();
};
}
app.post('/users', validateBody(userSchema), async (req, res) => {
const user = await db.createUser(req.body);
res.json(user);
});
`
Python: jsonschema
<code>bash
<p>pip install jsonschema</p>
</code>
`python
from jsonschema import validate, ValidationError, Draft7Validator
schema = {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string", "format": "email"}
}
}
Simple validation (raises on failure)
try:
validate(instance=data, schema=schema)
except ValidationError as e:
print(f"Error at {list(e.path)}: {e.message}")
Collect all errors
validator = Draft7Validator(schema)
errors = list(validator.iter_errors(data))
for error in errors:
print(f"{list(error.path)}: {error.message}")
`
Reusable schemas with $defs
<code>json
<p>{</p>
<p>"$defs": {</p>
<p>"Address": {</p>
<p>"type": "object",</p>
<p>"properties": {</p>
<p>"street": {"type": "string"},</p>
<p>"city": {"type": "string"},</p>
<p>"country": {"type": "string", "minLength": 2, "maxLength": 2}</p>
<p>},</p>
<p>"required": ["street", "city", "country"]</p>
<p>},</p>
<p>"Money": {</p>
<p>"type": "object",</p>
<p>"properties": {</p>
<p>"amount": {"type": "number", "minimum": 0},</p>
<p>"currency": {"type": "string", "enum": ["USD", "EUR", "GBP"]}</p>
<p>},</p>
<p>"required": ["amount", "currency"]</p>
<p>}</p>
<p>},</p>
<p>"type": "object",</p>
<p>"properties": {</p>
<p>"billingAddress": {"$ref": "#/$defs/Address"},</p>
<p>"shippingAddress": {"$ref": "#/$defs/Address"},</p>
<p>"totalPrice": {"$ref": "#/$defs/Money"}</p>
<p>}</p>
<p>}</p>
</code>
Conditional validation
<code>json
<p>{</p>
<p>"type": "object",</p>
<p>"properties": {</p>
<p>"customerType": {"type": "string", "enum": ["individual", "business"]},</p>
<p>"firstName": {"type": "string"},</p>
<p>"companyName": {"type": "string"},</p>
<p>"vatNumber": {"type": "string"}</p>
<p>},</p>
<p>"if": {</p>
<p>"properties": {"customerType": {"const": "business"}}</p>
<p>},</p>
<p>"then": {</p>
<p>"required": ["companyName", "vatNumber"]</p>
<p>},</p>
<p>"else": {</p>
<p>"required": ["firstName"]</p>
<p>}</p>
<p>}</p>
</code>
Generating schemas from TypeScript with zod
Zod is increasingly popular for defining schemas in TypeScript with runtime validation:
<code>bash
<p>npm install zod</p>
</code>
`typescript
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user', 'guest'])
});
type User = z.infer; // TypeScript type inference
// Validation
const result = UserSchema.safeParse(requestBody);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = result.data; // Fully typed and validated
``
Zod provides TypeScript type inference alongside validation — the schema and the type stay in sync automatically.
JSON Schema validation is best added at API boundaries — the entry points where external data enters your system. Internal function-to-function calls generally don't need schema validation (TypeScript's type system handles that). Validate at the edge; trust typed internal interfaces everywhere else.
Originally published at https://snappytools.app/json-schema-validator/