JSON Schema: Validating API Data with ajv and Python jsonschema

posted 5 min read

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/

More Posts

Merancang Backend Bisnis ISP: API Pelanggan, Paket Internet, Invoice, dan Tiket Support

Masbadar - Mar 13

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelskiverified - Mar 19

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Dharanidharan - Mar 3

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

Sovereign Intelligence: The Complete 25,000 Word Blueprint (Download)

Pocket Portfolio - Apr 1
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

3 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!