Treat your OpenAPI as a contract, not a suggestion. Use JSON Schema patterns like
$ref,allOf/oneOf,readOnly/writeOnly, and strong validation to ship trustworthy docs—without turning your spec into spaghetti.
Why this guide?
Most “API docs” explain the endpoints and forget the data contract. That’s where things go sideways. The fastest way to make your docs self-explanatory (and your API safer) is to model your payloads well—with JSON Schema inside OpenAPI.
This post shows practical patterns you can copy into your spec today.
OpenAPI + JSON Schema: what goes where?
- OpenAPI describes the surface: paths, operations, parameters, auth, servers, etc.
- JSON Schema describes the shape of data: types, constraints, composition, examples.
If OpenAPI is the map, JSON Schema is the terrain—don’t skimp on the terrain.
Quick note on versions
-
OpenAPI 3.0.x: JSON Schema subset.
nullable: trueexists. Some advanced keywords are limited. -
OpenAPI 3.1: Aligns with JSON Schema 2020-12. Use
type: [ "string", "null" ]instead ofnullable. Supports modern keywords likeif/then/elseandunevaluatedProperties.
When starting from scratch, choose 3.1.
A tiny yet solid starter
openapi: 3.1.0
info:
title: Task API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/tasks:
post:
summary: Create a task
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewTask'
examples:
minimal:
value: { title: "Ship v2", dueDate: "2025-09-01" }
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
components:
schemas:
ID:
type: string
description: Snowflake/UUID
pattern: '^[A-Za-z0-9_-]{10,}$'
NewTask:
type: object
required: [title]
additionalProperties: false
properties:
title:
type: string
minLength: 3
maxLength: 120
description: Human-readable title
dueDate:
type: [string, 'null']
format: date
description: Optional due date (YYYY-MM-DD)
Task:
allOf:
- $ref: '#/components/schemas/NewTask'
- type: object
required: [id, createdAt]
properties:
id:
$ref: '#/components/schemas/ID'
readOnly: true
createdAt:
type: string
format: date-time
readOnly: true
Why it works
-
$ref+allOfkeeps models DRY. -
additionalProperties: falseprevents accidental fields from sneaking in. -
readOnlykeeps server-computed fields out of requests.
Reuse without pain: $ref, allOf, oneOf, anyOf
$ref for building blocks
Create small, trustworthy primitives (Email, ID, Money) and reference them everywhere.
components:
schemas:
Email:
type: string
format: email
maxLength: 254
allOf for inheritance-ish reuse
Compose a specialized schema from a base.
AdminUser:
allOf:
- $ref: '#/components/schemas/User'
- type: object
properties:
roles:
type: array
items:
type: string
enum:
- admin
- auditor
oneOf for polymorphism (+ discriminator)
Make responses explicit when multiple shapes are possible.
PaymentMethod:
oneOf:
- $ref: '#/components/schemas/Card'
- $ref: '#/components/schemas/Bank'
discriminator:
propertyName: type
mapping:
card: '#/components/schemas/Card'
bank: '#/components/schemas/Bank'
This drives better examples in Swagger UI and prevents ambiguous payloads.
Validation is a feature (not paperwork)
Enforce your business rules in the schema. Your API becomes self-validating and your docs become executable.
-
Strings:
minLength,maxLength,pattern -
Numbers:
minimum,maximum,multipleOf -
Arrays:
uniqueItems,minItems,maxItems - Enums: use for status/rule vocabularies
-
Dates: prefer
format: date/date-timeto plain strings -
Conditionals (3.1):
if/then/elsefor business logic
Discount:
type: object
required:
- type
- value
properties:
type:
type: string
enum:
- percent
- fixed
value:
type: number
minimum: 0
if:
properties:
type:
const: percent
then:
properties:
value:
maximum: 100
Request vs. Response: use readOnly / writeOnly
Avoid duplicating schemas by flagging directionality.
User:
type: object
required:
- email
properties:
email:
$ref: '#/components/schemas/Email'
writeOnly: true # accepted on create/update, never returned
id:
$ref: '#/components/schemas/ID'
readOnly: true # returned by server only
Examples that teach (not confuse)
Prefer a few good examples over many mediocre ones. Use examples: at the media-type level to show scenarios.
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Task'
examples:
happy:
summary: Minimal task
value:
id: "a1B2c3D4e5"
title: "Ship v2"
createdAt: "2025-08-24T12:00:00Z"
withDue:
value:
id: "Z9y8X7w6V5"
title: "QA pass"
dueDate: "2025-09-01"
createdAt: "2025-08-24T12:00:00Z"
Common pitfalls (and quick fixes)
Forgetting
additionalProperties
Default istrue. Set it explicitly to avoid silent data drift.Using
nullableon 3.1
Replacenullable: truewith:
type:
- string
- null
Over-ref’ing
$refeverything and you’ll create a maze. Keep primitives small and reuse intentionally.Confusing
examplevsexamples
Useexamples(plural) when you want named cases at the media-type level."format" as a validator
Treatformatas a hint. If it must be enforced, add apatternor a custom rule in your gateway/server.
Lint it, test it, ship it
Add guardrails so your spec doesn’t rot.
- Linting: Use a JSON/YAML linter for semantic rules (naming, enums, required fields). A simple rule set can catch 80% of mistakes.
- Contract tests: Validate real payloads against your schemas in CI.
- Docs preview: Render locally (Theneo/Swagger UI/Redoc/etc.) and verify examples look right.
- Versioning: Treat breaking changes seriously—add new fields as optional first, deprecate old ones, then remove in a major version.
Pro tip: In CI, validate both the spec and a small library of golden request/response samples. If a sample drifts, your PR fails.
Copy-paste checklist
- [ ] OpenAPI 3.1 (unless you have a hard 3.0 reason)
- [ ] DRY:
$refsmall building blocks,allOffor composition - [ ] Directional fields:
readOnly/writeOnly - [ ] Strong validation: lengths, ranges, patterns, enums
- [ ]
additionalProperties: falsewhere appropriate - [ ] Named
examplesthat mirror real scenarios - [ ] Lint + validate in CI
- [ ] Document deprecations and versioning strategy
Tools & References
Authoring, linting & docs
Theneo — AI-assisted API editor & docs that stay in sync with your OpenAPI; collaborative editing, reviews, and branded portals.
Swagger Editor — Open-source editor for writing/validating OpenAPI in YAML/JSON.
Redoc / Redocly — High-quality static rendering for OpenAPI, easy theming and hosting.
Spectral — JSON/YAML linter to enforce naming, style, and consistency in specs.
Stoplight Studio — GUI modeling/editor with mocking and collaboration features.
Postman — API client with schema sync, tests, and collections generated from OpenAPI.
Mocking, testing & code generation
Prism — Mock server that serves responses directly from your OpenAPI.
Dredd — Contract testing to verify your API implementation against the spec.
Schemathesis — Property-based testing that uses your schema to find edge cases.
openapi-generator — Generates client SDKs and server stubs for many languages.
Specs & references
OpenAPI Specification 3.1 — Latest version aligned with JSON Schema.
JSON Schema (2020-12) — Reference for validation keywords and composition.
Shout-out: these patterns mirror what we use with Theneo to keep large specs clean, validated, and easy to publish.
Wrap-up
Great API docs aren’t about pretty UIs—they’re about precise data contracts. Model your payloads with JSON Schema, wire them into OpenAPI, and let tools do the heavy lifting. Your consumers (and future you) will thank you.
Top comments (0)