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: true
exists. 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/else
andunevaluatedProperties
.
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
+allOf
keeps models DRY. -
additionalProperties: false
prevents accidental fields from sneaking in. -
readOnly
keeps 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-time
to plain strings -
Conditionals (3.1):
if
/then
/else
for 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
nullable
on 3.1
Replacenullable: true
with:
type:
- string
- null
Over-ref’ing
$ref
everything and you’ll create a maze. Keep primitives small and reuse intentionally.Confusing
example
vsexamples
Useexamples
(plural) when you want named cases at the media-type level."format" as a validator
Treatformat
as a hint. If it must be enforced, add apattern
or 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:
$ref
small building blocks,allOf
for composition - [ ] Directional fields:
readOnly
/writeOnly
- [ ] Strong validation: lengths, ranges, patterns, enums
- [ ]
additionalProperties: false
where appropriate - [ ] Named
examples
that 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)