DEV Community

arobakid
arobakid

Posted on

Stop Hand-Waving Your API: Practical JSON Schema Patterns in OpenAPI

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 of nullable. Supports modern keywords like if/then/else and unevaluatedProperties.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Common pitfalls (and quick fixes)

  • Forgetting additionalProperties

    Default is true. Set it explicitly to avoid silent data drift.

  • Using nullable on 3.1

    Replace nullable: true with:

type:
  - string
  - null
Enter fullscreen mode Exit fullscreen mode
  • Over-ref’ing

    $ref everything and you’ll create a maze. Keep primitives small and reuse intentionally.

  • Confusing example vs examples

    Use examples (plural) when you want named cases at the media-type level.

  • "format" as a validator

    Treat format as a hint. If it must be enforced, add a pattern 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)