DEV Community

Deepak Kumar
Deepak Kumar

Posted on Originally published at jsonformatterhub.com

JSON Schema — How to Validate Your JSON Data Structure

JSON doesn't enforce anything on its own. You can send {"age": "twelve"} to an endpoint expecting a number and the format won't complain — catching that is on you. JSON Schema is the standard way to stop doing that in scattered application code. It's a document, written in JSON itself, that describes the exact shape another JSON object must have: which fields are required, what types they hold, what patterns strings must match. Define it once and any conforming validator handles the checking — in JavaScript, Python, Java, Go, and most other languages with a maintained implementation.

The current spec version is draft 2020-12, published as an IETF draft. The version matters because keyword behavior changed between drafts — more on that below.

The $schema and $id Keywords

Every schema should start with two meta-keywords. $schema declares which version of the spec the document follows. $id gives the schema a canonical URI — useful when schemas reference each other or are published for others to consume.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/user-registration.json",
  "title": "User Registration",
  "description": "Schema for a new user sign-up payload",
  "type": "object"
}
Enter fullscreen mode Exit fullscreen mode

Neither keyword is required for a schema to work locally, but including them is considered best practice and makes schemas self-documenting.

Core Keywords: type, properties, required, additionalProperties

type is the most fundamental constraint. Valid values are "string", "number", "integer", "boolean", "array", "object", and "null". You can also pass an array of types when a field may legitimately hold more than one, such as ["string", "null"] for a nullable field.

For objects, properties defines a schema for each named key, while required lists the keys that must be present. Note carefully: required only checks presence, not the value — a required field can still be null unless you also constrain its type. Setting additionalProperties to false rejects any key not listed under properties, useful for strict API contracts.

{
  "type": "object",
  "properties": {
    "username": { "type": "string" },
    "email": { "type": "string" },
    "age": { "type": "integer" }
  },
  "required": ["username", "email"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

String Constraints

minLength and maxLength set character-count bounds. pattern accepts a regular expression (ECMA 262 dialect) the string must match. enum restricts the value to a fixed list, and format declares a semantic format like "email", "uri", "date", or "uuid" — though format validation is optional in many implementations and must be explicitly enabled.

{
  "username": {
    "type": "string",
    "minLength": 3,
    "maxLength": 30,
    "pattern": "^[a-zA-Z0-9_-]+$"
  },
  "email": {
    "type": "string",
    "format": "email"
  },
  "role": {
    "type": "string",
    "enum": ["admin", "editor", "viewer"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Number Constraints

Numbers can be bounded with minimum, maximum, exclusiveMinimum, and exclusiveMaximum. multipleOf enforces divisibility — useful for things like page sizes that must be multiples of 10, or currency amounts that must be multiples of 0.01.

{
  "age": {
    "type": "integer",
    "minimum": 13,
    "maximum": 120
  },
  "score": {
    "type": "number",
    "minimum": 0,
    "maximum": 100,
    "multipleOf": 0.5
  }
}
Enter fullscreen mode Exit fullscreen mode

Array Validation

items provides a schema every element must satisfy. minItems and maxItems control array length, and uniqueItems: true requires no two elements be identical — handy for tag lists or permission sets.

{
  "tags": {
    "type": "array",
    "items": { "type": "string", "minLength": 1, "maxLength": 50 },
    "minItems": 1,
    "maxItems": 10,
    "uniqueItems": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Nested Schemas with $ref and $defs

Real schemas quickly grow complex. $defs (called definitions in older drafts) lets you define reusable sub-schemas in one place and reference them with $ref — avoiding copy-pasting the same address schema into every object that has a billing or shipping address.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string", "minLength": 2, "maxLength": 2 }
      },
      "required": ["street", "city", "country"]
    }
  },
  "type": "object",
  "properties": {
    "billingAddress": { "$ref": "#/$defs/address" },
    "shippingAddress": { "$ref": "#/$defs/address" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Complete Example: User Registration Payload

A realistic schema for a user registration API endpoint, combining type constraints, string validation, and required fields into a document you could drop into a validator today:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/register.json",
  "title": "User Registration",
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 30,
      "pattern": "^[a-zA-Z0-9_-]+$"
    },
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 254
    },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128
    },
    "age": {
      "type": "integer",
      "minimum": 13
    },
    "newsletter": {
      "type": "boolean"
    },
    "interests": {
      "type": "array",
      "items": { "type": "string" },
      "maxItems": 20,
      "uniqueItems": true
    }
  },
  "required": ["username", "email", "password"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

Validating with Ajv (JavaScript)

Ajv is the most widely used JSON Schema validator for JavaScript and Node.js. It compiles schemas to optimized validation functions, so repeated validations on the same schema are very fast. Install with npm install ajv:

import Ajv from "ajv";
import addFormats from "ajv-formats"; // for "email", "uri", etc.

const ajv = new Ajv();
addFormats(ajv);

const schema = {
  type: "object",
  properties: {
    username: { type: "string", minLength: 3 },
    email: { type: "string", format: "email" }
  },
  required: ["username", "email"]
};

const validate = ajv.compile(schema);

const data = { username: "alice", email: "alice@example.com" };
const valid = validate(data);

if (!valid) {
  console.error(validate.errors);
} else {
  console.log("Data is valid");
}
Enter fullscreen mode Exit fullscreen mode

The validate.errors array contains structured error objects with a message, a JSON Pointer instancePath pointing to the failing field, and a keyword indicating which constraint was violated — making it straightforward to map errors back to form fields in a UI.

Validating with jsonschema (Python)

The jsonschema library is the standard choice in Python. Install with pip install jsonschema. The simplest usage calls validate(), which raises a ValidationError on failure:

import json
from jsonschema import validate, ValidationError

schema = {
    "type": "object",
    "properties": {
        "username": {"type": "string", "minLength": 3},
        "email": {"type": "string", "format": "email"}
    },
    "required": ["username", "email"]
}

payload = {"username": "alice", "email": "alice@example.com"}

try:
    validate(instance=payload, schema=schema)
    print("Valid")
except ValidationError as e:
    print(f"Invalid: {e.message}")
    print(f"Field path: {list(e.absolute_path)}")
Enter fullscreen mode Exit fullscreen mode

For validating many documents against the same schema, create a jsonschema.Draft202012Validator instance once and call its validate() method repeatedly to avoid re-parsing the schema on every call.

Testing Your Schemas

Before wiring a schema into application code, test it manually with sample payloads — both valid ones and deliberately broken ones that should fail. Paste your JSON into a formatter first to ensure it's well-formed before feeding it to a validator; catching a missing comma in your test data first saves confusion about whether the schema or the data is wrong.

JSON Schema is a powerful contract mechanism that moves validation logic out of imperative code and into a declarative, language-agnostic document. Once you have a schema, you can share it with API consumers, generate documentation from it, use it in CI to gate malformed requests, and even derive TypeScript types from it with tools like json-schema-to-typescript.


If you're testing a schema right now, JSON Formatter Hub formats and validates JSON entirely in your browser — a quick way to confirm your test payloads are well-formed before you throw them at a validator.

Top comments (0)