Writing Your First JSON Schema From a Real Document
Build a JSON Schema from a user profile document one keyword at a time, run it in Python after each step, and catch typos that a validator ignores silently.

The shortest path to understanding JSON Schema is to write one for a document you already have, and to run a validator after every line. Reading the specification first leads to a schema that nobody tested. This article builds a schema for a small user profile in four steps, shows the exact error messages the Python jsonschema library gives at each step, and ends with the checks for the schema itself, because a schema with a typo is accepted without complaint and then checks nothing.
The document
Here is the document the schema must describe:
{
"name": "Ada",
"age": 36,
"email": "[email protected]",
"roles": ["admin", "dev"]
}Before writing anything, decide the rules in plain words, because the schema is a translation of them. A profile is an object. name is required, a non-empty string. email is required and must look like an email address. age is optional, a whole number from 0 to 150. roles is an optional list of known role names without repeats. Nothing else is allowed. Each sentence becomes one or two keywords.
Step 1: the type of the whole document
from jsonschema import Draft202012Validator
schema = {"type": "object"}
Draft202012Validator(schema).is_valid(doc)The profile is valid, and [1] fails with [1] is not of type 'object'. This is the weakest useful schema: it rejects arrays, strings and numbers at the top level, which is already worth something when an endpoint receives whatever a client sends. Add $schema at the top of the file to say which draft you are writing for, "$schema": "https://json-schema.org/draft/2020-12/schema", so that editors and other tools interpret the keywords as you do.
Step 2: describe the properties
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
}Now {"name": 5, "age": "x"} fails twice: [$.age] 'x' is not of type 'integer' and [$.name] 5 is not of type 'string'. The paths in square brackets are JSON paths into the document, which is how you find the offending value in a large one. But the empty object {} is valid. That surprises most people the first time. properties is a set of rules for the keys that happen to be present; it does not demand that any key exists. A document with none of the listed keys has nothing to break.
Step 3: say what is required
schema["required"] = ["name"]Now {} fails with [$] 'name' is a required property and {"name": "Ada"} passes. required is a list of key names on the object where it appears, and it sits next to properties, not inside a property. A common error is to write "required": true inside a property's schema, which is the older draft 3 style and means nothing in current drafts. The details, including how a key present with a null value differs from an absent key, are in required fields and optional fields in JSON Schema.
Step 4: add the constraints
With the structure right, add the limits from the plain-words list:
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"email": {"type": "string", "format": "email"},
"roles": {
"type": "array",
"items": {"enum": ["admin", "dev", "ops"]},
"uniqueItems": True,
},
},
"additionalProperties": False,
}The good document passes. For a bad one I wrote {"name": "", "age": -3, "email": "ada", "roles": ["admin", "root", "admin"], "extra": 1} and got six errors from a single validation:
[$.age] -3 is less than the minimum of 0
[$.email] 'ada' is not a 'email'
[$.name] '' should be non-empty
[$.roles[1]] 'root' is not one of ['admin', 'dev', 'ops']
[$.roles] ['admin', 'root', 'admin'] has non-unique elements
[$] Additional properties are not allowed ('extra' was unexpected)Each message names a path and a rule, and each rule maps to a keyword you wrote. The pattern to follow in your own work is the same: add one keyword, then craft a document that should fail because of that keyword and confirm it does. If you cannot make a bad document fail, the keyword is not doing what you think.
Two keywords need a note. additionalProperties: false turns the unknown key extra into an error. It is the right setting for an input you fully control, and it makes every future field a breaking change for consumers holding the old schema (see versioning a JSON Schema without breaking consumers). format: email is only a rough check, and whether it is checked at all depends on the library and its optional packages, as discussed in using JSON Schema enums, formats and patterns.
Check the schema itself
A schema is a JSON document that can be wrong, and a validator will happily run a wrong one. Two cases I tried:
Draft202012Validator.check_schema({"type": "objekt"})
# SchemaError: 'objekt' is not valid under any of the given schemas
Draft202012Validator.check_schema({"properties": {"a": {"minimum": "5"}}})
# SchemaError: '5' is not of type 'number'check_schema validates your schema against the specification's meta-schema, so invalid values for known keywords are caught. What it cannot catch is a keyword it does not know. I validated the empty object {} against {"type": "object", "requried": ["x"]} and it passed as OK: the misspelled requried is treated as an annotation and ignored, as the specification requires of unknown keywords. A schema with that typo accepts every document and looks correct. Ajv in Node is stricter and throws on unknown keywords by default (strict mode: unknown keyword: "colour"), which is one reason to run a schema through it in CI even if the production validator is Python.
The defense that works in any language is a test file: a few documents that must pass, and a few that must fail, each broken in one named way. If requried had been in your schema, the document missing email would have passed, and the test that expects it to fail would have caught the typo the same day.
Put the schema in a file
A schema in the middle of Python code is hard to share. Save it as profile.schema.json and load it:
import json
schema = json.load(open("profile.schema.json"))
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)The same file can be used by a JavaScript service, by editors that offer autocompletion when a JSON file refers to it through $schema, and by CI jobs. For a command line check there is check-jsonschema --schemafile profile.schema.json doc.json, which printed data/one.json::$: 'ts' is a required property style lines and exited with status 1 on my invalid test file, and ok -- validation done with status 0 on a valid one. The older python -m jsonschema command still works but prints a deprecation warning that points to check-jsonschema. The JSON schema generator can draft a first version from your document if you prefer to edit rather than type, and generating a JSON Schema from sample data explains what such a draft gets wrong.
What to write down as you go
Write a short description on each property ("description": "Shown on the public profile"), and a title on the schema. They do not affect validation, and they turn the schema into documentation that editors show on hover and that API documentation tools render. Add examples for fields where a sample value clarifies the rule. Keep the plain-words list from the start of this exercise somewhere near the schema, since it records why each limit exists, and that is the part people forget.
When a first schema is not enough
The document has variants. A profile for a person and a profile for a company share some fields and differ in others. That needs oneOf or if/then, and a first-schema approach of one flat list of properties will either reject valid documents or accept invalid ones (validating JSON with JSON Schema in real projects lists the combining keywords).
The rule involves two fields. "End date after start date" is not expressible. Check it in code after the schema passes.
Documents repeat the same shape in many places. Move the repeated part to $defs and refer to it (reusing definitions with $ref in JSON Schema).
The keywords are defined in the JSON Schema specification, and the learning material at Understanding JSON Schema is a good companion. Paste your document into the JSON formatter before you start, to make sure it is valid JSON at all.


