Writing OpenAPI Specs That Generate Better Docs
A structurally valid spec and a genuinely useful one are different documents. Here's what separates them, field by field.
Structurally Valid Isn't the Same as Useful
A spec with every required field present and every schema correctly typed will pass validation and render in Swagger UI or Redoc without complaint. It will also, very often, produce documentation that says almost nothing: an endpoint titled POST /orders with no summary, a request body with no examples, and a 200 response described only as "Success." Validation checks structure, not usefulness — and usefulness is what determines whether a developer (or an AI agent) can actually call your API correctly on the first try. The fields below are the ones that move the needle most, roughly in order of impact per minute spent.
summary vs. description: Use Both, Differently
Every Operation Object accepts both summary and description, and treating them as duplicates wastes one of them. summary is a single short line — Redoc and Swagger UI show it in the endpoint list where a whole spec's worth of operations are scanned at a glance, so it needs to work as a label:
paths:
/orders/{orderId}/refund:
post:
summary: Issue a refund for an order
description: |
Issues a full or partial refund for a completed order. Refunds
are asynchronous — the response returns a `pending` refund
object immediately, and a `refund.completed` webhook fires
once the payment provider confirms the reversal, typically
within a few seconds but occasionally up to 24 hours for
bank transfers.
Partial refunds accept an `amount` field; omitting it
refunds the order in full. A refund cannot be issued twice
for the same order — a second call returns 409.description supports full CommonMark and is where the actual behavior lives: async timing, idempotency rules, edge cases, what happens on a repeat call. A generated SDK method comment usually pulls from description; the method list in an IDE's autocomplete usually shows summary. Skip description and both are worse: the summary alone can't carry "refunds are async and idempotent," and a caller who doesn't read prose documentation (most people, and virtually every AI agent operating without a human in the loop) will get the async behavior wrong at least once.
examples: Show, Don't Just Type
A schema declares shape; an example declares a plausible, concrete instance of that shape, and the two catch different classes of confusion. { "type": "string", "format": "date-time" } is correct but doesn't tell a caller whether to send 2026-07-18T14:30:00Z or 2026-07-18 14:30:00 — an example does, instantly.
Examples exist at two different levels with different keywords, and mixing them up is a common source of confusion:
- Media Type Object (request bodies, responses):
example(single) orexamples(a map of named Example Objects, useful for showing several valid variants — a minimal request vs. one using every optional field). - Schema Object: in OpenAPI 3.1, prefer
examplesas a plain array of values (the native JSON Schema 2020-12 keyword — see our 3.0 vs. 3.1 guide for why the singular schema-levelexampleis deprecated there).
responses:
'200':
description: The requested order
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
examples:
fulfilled:
summary: A completed, shipped order
value: { id: "ord_1a2b", status: "fulfilled", total: 4899 }
pending:
summary: An order awaiting payment
value: { id: "ord_9f8e", status: "pending", total: 1200 }Named, multi-variant examples like this are worth the extra nesting for any status field, enum, or polymorphic response — anywhere "what does a realistic instance of this actually look like" has more than one interesting answer.
Schema Annotations: title, description, deprecated
Every property in a components/schemas object can carry its own title and description, independent of the parent schema's. This is the field most often skipped because it's tedious at scale, and the one that most directly determines whether a generated SDK's field-level doc comments say anything:
Order:
type: object
properties:
id:
type: string
description: Opaque order identifier, prefixed "ord_".
total:
type: integer
description: Total charge in the smallest currency unit (cents for USD).
legacyStatus:
type: string
deprecated: true
description: Use `status` instead. Retained for clients on API v1.deprecated: true on a property or an entire operation isn't decorative — Swagger UI strikes through deprecated fields visually, and SDK generators commonly emit a language-appropriate deprecation annotation (a @deprecated Javadoc tag, Python's warnings.deprecated, TypeScript's @deprecated JSDoc tag) that surfaces a warning directly in a caller's editor. That's a materially better deprecation channel than a changelog entry nobody reads, and it costs one line per field.
Ambiguous numeric units are worth calling out explicitly, as above with total — "cents, not dollars" is exactly the kind of fact a type signature can't carry but a one-line description can, and it's a genuinely common integration bug when left unstated.
Tags and Tag Descriptions
The top-level tags array is easy to treat as optional metadata, but it's what turns a flat list of forty operations into a navigable, grouped document:
tags:
- name: Orders
description: Create, retrieve, and refund customer orders.
- name: Webhooks
description: Manage webhook subscriptions for asynchronous order events.Without a top-level tags entry, an operation-level tags: [Orders] still groups endpoints in generated docs, but the group heading has no description — a missed opportunity to explain, once, what an entire resource family is for instead of repeating context in every operation's description. Tags with no matching operations, or operations referencing an undeclared tag, are exactly the kind of mismatch our common validation errors guide covers.
Why This Matters More for AI Agents Than for Humans
A human reading thin documentation falls back on trial and error, reading response bodies, or asking a colleague. An AI agent calling your API from the spec alone — increasingly common as MCP servers and tool-calling agents consume OpenAPI documents directly as their entire interface definition — has no fallback beyond what's in the document. It can't infer that total is in cents, that a refund is idempotent, or which of three similarly-named endpoints is the current one versus the deprecated one, unless the spec says so explicitly. The fields in this guide aren't documentation polish in that context — for an agent with no other source of truth, they are the entire contract, and a poorly annotated spec is functionally equivalent to an undocumented one no matter how complete its paths object is.
Check how your own spec renders and validate it for structural gaps with our OpenAPI / Swagger Viewer. Schema fragments are easiest to iterate on with a dedicated formatter — our JSON Formatter & Validator catches syntax slips before they reach the spec. And if your response descriptions are vague about what a given status code actually means for your API, the HTTP Status Code reference is a fast way to write ones that are precise instead of generic.
Further Reading
- OpenAPI Specification v3.1.0 — Operation Object
Authoritative definition of summary, description, deprecated, and tags on an operation.
- Swagger — Adding Examples
Swagger's own guide to the example vs. examples distinction at the media type and schema levels.
- JSON Schema — Annotations (title, description, deprecated)
How JSON Schema annotation keywords are defined and consumed by tooling, which OpenAPI 3.1 schemas inherit directly.
- OpenAPI Initiative blog
The OpenAPI Initiative's own posts on spec authoring practices and tooling ecosystem updates.