JSON Schema Author
Overview
Keywords: JSON Schema, Draft 2020-12, Draft-07, $schema, $ref, $defs, validation, config schema, API schema, OpenAPI, additionalProperties, oneOf, anyOf, allOf, if/then/else, format, pattern, const, enum, error messages, ajv, jsonschema.
This skill helps you design correct, strict, and maintainable JSON Schemas for two dominant use cases: configuration files (validate user/operator input, fail fast with helpful messages) and APIs (contract for request/response bodies, often embedded in OpenAPI). It covers the type system, every important constraint keyword, schema composition, reuse via $ref/$defs, conditional validation, and turning raw validator output into actionable error messages.
Use the bundled material:
references/keywords.md — complete keyword reference (types, constraints, composition, applicators, annotations, draft differences) with copy-paste snippets.
references/error-messages.md — patterns for clear, user-facing validation errors and how to map validator output to them.
templates/config-schema.json — a strict, annotated starting point for a config-file schema.
templates/api-schema.json — request/response schema pattern suitable for OpenAPI components.schemas.
scripts/validate.py — stdlib-only structural linter that flags the most common schema authoring mistakes; falls back to jsonschema if installed for real validation.
examples/config-walkthrough.md — sample config JSON turned into a finished schema with passing and failing instances.
Workflow
Pick the draft. Default to Draft 2020-12 for new schemas ($schema: "https://json-schema.org/draft/2020-12/schema"). Use Draft-07 when targeting tooling that lags (older ajv configs, many OpenAPI 3.0 toolchains). OpenAPI 3.1 aligns with 2020-12. Note the differences: 2020-12 uses $defs and prefixItems/items; Draft-07 uses definitions and items (array)/additionalItems. See references/keywords.md.
Identify the root type and intent. Is this a config object, an API request, an API response, or a reusable component? Decide whether unknown keys are errors (config → almost always "additionalProperties": false) or tolerated (forward-compatible APIs may allow them).
Model the shape. Define type, then properties, then required. List every known property. Add a one-line description and a realistic examples/default where it aids users and generated docs.
Add constraints, narrowest first. Apply enum/const, numeric bounds (minimum, maximum, exclusiveMinimum, multipleOf), string rules (minLength, maxLength, pattern, format), and array/object cardinality (minItems, uniqueItems, minProperties). Prefer enum over free-form strings when the value set is closed.
Compose and reuse. Extract repeated shapes into $defs and reference with $ref. Use allOf to combine/extend, oneOf for mutually-exclusive variants (tagged unions), anyOf for "at least one", and if/then/else for conditional requirements. Avoid oneOf when only one branch can plausibly match — prefer a discriminator pattern (see references/keywords.md).
Lock it down. Set additionalProperties: false for configs and internal APIs. For arrays, decide additionalItems/items: false if a tuple. Mark deprecated fields with deprecated: true.
Write good error messaging. Decide between validator-native errors and curated messages. Add title/description and, where supported, errorMessage (ajv-errors) or a post-processing map. Follow references/error-messages.md.
Validate against real instances. Run scripts/validate.py to lint the schema structure, then validate at least one passing and several failing instances (one per constraint). Never ship a schema without a failing-case test.
Decision framework: choosing composition keywords
| Need |
Keyword |
Notes |
| Exactly one of several variants |
oneOf |
Validation fails if zero or 2+ match. Add a discriminating const per branch. |
| At least one variant (overlap OK) |
anyOf |
Cheaper, clearer than oneOf when overlap is allowed. |
| Combine constraints (extend a base) |
allOf |
All subschemas must pass. Beware additionalProperties interactions. |
| Required field depends on another field's value |
if/then/else |
e.g. if type=="ssl" then cert is required. |
| Field A requires field B present |
dependentRequired |
Simple co-presence rules. |
| Closed value set |
enum / const |
const for single value; enum for a list. |
Critical gotcha: additionalProperties: false only sees properties declared in the same schema object, not those introduced by allOf/$ref siblings. To extend strictly, either declare all properties locally or use unevaluatedProperties: false (Draft 2019-09+/2020-12). See references/keywords.md.
Worked example (tagged union)
A notification config where the payload depends on channel:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["channel"],
"properties": { "channel": { "enum": ["email", "sms"] } },
"oneOf": [
{
"properties": {
"channel": { "const": "email" },
"to": { "type": "string", "format": "email" }
},
"required": ["to"]
},
{
"properties": {
"channel": { "const": "sms" },
"to": { "type": "string", "pattern": "^\\+[1-9]\\d{6,14}$" }
},
"required": ["to"]
}
],
"unevaluatedProperties": false
}
Each branch pins channel with const, so exactly one branch can match — clean error attribution. See examples/config-walkthrough.md for a full multi-property example.
Best Practices
- Always declare
$schema. It tells validators and humans the dialect.
- Be strict by default.
additionalProperties: false for configs; require what's mandatory. Loosen deliberately, not accidentally.
- One source of truth per shape. Factor repeated structures into
$defs and $ref them.
- Constrain values, not just types. A
"type": "string" port number is a bug; use "type": "integer", "minimum": 1, "maximum": 65535.
- Prefer
enum to pattern when the set is finite — it yields better errors and docs.
- Annotate for humans and generators.
title, description, examples, default, deprecated feed docs, IDE autocomplete, and form generators.
- Use
format judiciously. Many validators treat format as annotation-only unless explicitly enabled (e.g. ajv formats, format: "full"). Back security-critical formats with a pattern.
- Pin tag fields with
const inside oneOf branches so exactly one branch matches and errors point to the right place.
- Test failing instances. Each constraint deserves a test that proves it rejects bad input.
Common Pitfalls
additionalProperties: false + allOf/$ref silently rejects inherited properties. Use unevaluatedProperties: false instead when extending.
- Mixing draft idioms. Don't use
definitions (Draft-07) and $defs (2020-12) inconsistently, or items: [..] tuple syntax under 2020-12 (use prefixItems).
required does not imply existence checks for nested objects unless you also constrain the nested schema — list required at each level.
oneOf with non-exclusive branches fails when input matches two branches. Add discriminators.
- Forgetting
type lets unexpected JSON types pass (a number where you meant a string).
format assumed enforced. It's often advisory; verify your validator enforces it or add a pattern.
- Numbers vs integers.
"type": "number" accepts 1.5; use "integer" for counts/ports/IDs.
- Unescaped regex in
pattern. JSON requires \\ for a single backslash; \d must be written \\d.
- Open
enum drift. When the value set grows, update the schema — stale enums reject valid new values.
1---2name: json-schema-author3description: Authors and validates JSON Schema (Draft 2020-12 and Draft-07) for configuration files and HTTP/REST APIs, applying types, constraints, composition keywords, and human-readable error messaging. Use this skill when the user wants to write, fix, refactor, or validate a JSON Schema, define a config-file contract, document request/response payloads, add validation rules to an OpenAPI spec, generate schemas from sample JSON, or produce clear validation error messages.4license: MIT5---67# JSON Schema Author89## Overview10Keywords: JSON Schema, Draft 2020-12, Draft-07, $schema, $ref, $defs, validation, config schema, API schema, OpenAPI, additionalProperties, oneOf, anyOf, allOf, if/then/else, format, pattern, const, enum, error messages, ajv, jsonschema.1112This skill helps you design correct, strict, and maintainable JSON Schemas for two dominant use cases: **configuration files** (validate user/operator input, fail fast with helpful messages) and **APIs** (contract for request/response bodies, often embedded in OpenAPI). It covers the type system, every important constraint keyword, schema composition, reuse via `$ref`/`$defs`, conditional validation, and turning raw validator output into actionable error messages.1314Use the bundled material:15- `references/keywords.md` — complete keyword reference (types, constraints, composition, applicators, annotations, draft differences) with copy-paste snippets.16- `references/error-messages.md` — patterns for clear, user-facing validation errors and how to map validator output to them.17- `templates/config-schema.json` — a strict, annotated starting point for a config-file schema.18- `templates/api-schema.json` — request/response schema pattern suitable for OpenAPI `components.schemas`.19- `scripts/validate.py` — stdlib-only structural linter that flags the most common schema authoring mistakes; falls back to `jsonschema` if installed for real validation.20- `examples/config-walkthrough.md` — sample config JSON turned into a finished schema with passing and failing instances.2122## Workflow23241. **Pick the draft.** Default to **Draft 2020-12** for new schemas (`$schema: "https://json-schema.org/draft/2020-12/schema"`). Use **Draft-07** when targeting tooling that lags (older `ajv` configs, many OpenAPI 3.0 toolchains). OpenAPI 3.1 aligns with 2020-12. Note the differences: 2020-12 uses `$defs` and `prefixItems`/`items`; Draft-07 uses `definitions` and `items` (array)/`additionalItems`. See `references/keywords.md`.25262. **Identify the root type and intent.** Is this a config object, an API request, an API response, or a reusable component? Decide whether unknown keys are errors (config → almost always `"additionalProperties": false`) or tolerated (forward-compatible APIs may allow them).27283. **Model the shape.** Define `type`, then `properties`, then `required`. List every known property. Add a one-line `description` and a realistic `examples`/`default` where it aids users and generated docs.29304. **Add constraints, narrowest first.** Apply `enum`/`const`, numeric bounds (`minimum`, `maximum`, `exclusiveMinimum`, `multipleOf`), string rules (`minLength`, `maxLength`, `pattern`, `format`), and array/object cardinality (`minItems`, `uniqueItems`, `minProperties`). Prefer `enum` over free-form strings when the value set is closed.31325. **Compose and reuse.** Extract repeated shapes into `$defs` and reference with `$ref`. Use `allOf` to combine/extend, `oneOf` for mutually-exclusive variants (tagged unions), `anyOf` for "at least one", and `if/then/else` for conditional requirements. Avoid `oneOf` when only one branch can plausibly match — prefer a discriminator pattern (see `references/keywords.md`).33346. **Lock it down.** Set `additionalProperties: false` for configs and internal APIs. For arrays, decide `additionalItems`/`items: false` if a tuple. Mark deprecated fields with `deprecated: true`.35367. **Write good error messaging.** Decide between validator-native errors and curated messages. Add `title`/`description` and, where supported, `errorMessage` (ajv-errors) or a post-processing map. Follow `references/error-messages.md`.37388. **Validate against real instances.** Run `scripts/validate.py` to lint the schema structure, then validate at least one passing and several failing instances (one per constraint). Never ship a schema without a failing-case test.3940## Decision framework: choosing composition keywords4142| Need | Keyword | Notes |43|------|---------|-------|44| Exactly one of several variants | `oneOf` | Validation fails if zero or 2+ match. Add a discriminating `const` per branch. |45| At least one variant (overlap OK) | `anyOf` | Cheaper, clearer than `oneOf` when overlap is allowed. |46| Combine constraints (extend a base) | `allOf` | All subschemas must pass. Beware `additionalProperties` interactions. |47| Required field depends on another field's value | `if`/`then`/`else` | e.g. if `type=="ssl"` then `cert` is required. |48| Field A requires field B present | `dependentRequired` | Simple co-presence rules. |49| Closed value set | `enum` / `const` | `const` for single value; `enum` for a list. |5051Critical gotcha: `additionalProperties: false` only sees properties declared in the **same** schema object, not those introduced by `allOf`/`$ref` siblings. To extend strictly, either declare all properties locally or use `unevaluatedProperties: false` (Draft 2019-09+/2020-12). See `references/keywords.md`.5253## Worked example (tagged union)5455A notification config where the payload depends on `channel`:5657```json58{59 "$schema": "https://json-schema.org/draft/2020-12/schema",60 "type": "object",61 "required": ["channel"],62 "properties": { "channel": { "enum": ["email", "sms"] } },63 "oneOf": [64 {65 "properties": {66 "channel": { "const": "email" },67 "to": { "type": "string", "format": "email" }68 },69 "required": ["to"]70 },71 {72 "properties": {73 "channel": { "const": "sms" },74 "to": { "type": "string", "pattern": "^\\+[1-9]\\d{6,14}$" }75 },76 "required": ["to"]77 }78 ],79 "unevaluatedProperties": false80}81```8283Each branch pins `channel` with `const`, so exactly one branch can match — clean error attribution. See `examples/config-walkthrough.md` for a full multi-property example.8485## Best Practices86- **Always declare `$schema`.** It tells validators and humans the dialect.87- **Be strict by default.** `additionalProperties: false` for configs; require what's mandatory. Loosen deliberately, not accidentally.88- **One source of truth per shape.** Factor repeated structures into `$defs` and `$ref` them.89- **Constrain values, not just types.** A `"type": "string"` port number is a bug; use `"type": "integer", "minimum": 1, "maximum": 65535`.90- **Prefer `enum` to `pattern`** when the set is finite — it yields better errors and docs.91- **Annotate for humans and generators.** `title`, `description`, `examples`, `default`, `deprecated` feed docs, IDE autocomplete, and form generators.92- **Use `format` judiciously.** Many validators treat `format` as annotation-only unless explicitly enabled (e.g. ajv `formats`, `format: "full"`). Back security-critical formats with a `pattern`.93- **Pin tag fields with `const`** inside `oneOf` branches so exactly one branch matches and errors point to the right place.94- **Test failing instances.** Each constraint deserves a test that proves it rejects bad input.9596## Common Pitfalls97- **`additionalProperties: false` + `allOf`/`$ref`** silently rejects inherited properties. Use `unevaluatedProperties: false` instead when extending.98- **Mixing draft idioms.** Don't use `definitions` (Draft-07) and `$defs` (2020-12) inconsistently, or `items: [..]` tuple syntax under 2020-12 (use `prefixItems`).99- **`required` does not imply existence checks for nested objects** unless you also constrain the nested schema — list `required` at each level.100- **`oneOf` with non-exclusive branches** fails when input matches two branches. Add discriminators.101- **Forgetting `type`** lets unexpected JSON types pass (a number where you meant a string).102- **`format` assumed enforced.** It's often advisory; verify your validator enforces it or add a `pattern`.103- **Numbers vs integers.** `"type": "number"` accepts `1.5`; use `"integer"` for counts/ports/IDs.104- **Unescaped regex in `pattern`.** JSON requires `\\` for a single backslash; `\d` must be written `\\d`.105- **Open `enum` drift.** When the value set grows, update the schema — stale enums reject valid new values.