Skip to main content

OpenAPI & Swagger Fundamentals

1. Swagger 2.0 vs OpenAPI 3.0 vs 3.1

FeatureSwagger 2.0OAS 3.0OAS 3.1
Server definitionhost, basePath, schemesservers[] with variablessame
Request bodyin: body parameterrequestBody with per-media-type schemasame
Reusable piecesdefinitions, parameters, responsescomponents/* (schemas, parameters, responses, examples, headers, securitySchemes, links, callbacks)adds pathItems
Nullablex-nullable vendor ext.nullable: truetype: ["string","null"] (JSON Schema)
PolymorphismallOf + discriminatoroneOf/anyOf/allOf + discriminatorsame
Callbacks / Webhooks-callbackstop-level webhooks
JSON SchemaSubset draft 4Modified subset draft 5Full 2020-12
Authbasic, apiKey, oauth2+ http (bearer), openIdConnect+ mutualTLS

Choose 3.1 for new APIs; check your generators (some still lag on 3.1 - verify before standardising).

2. Anatomy of an OpenAPI Document

openapi: 3.1.0
info:
title: Orders API
version: 1.4.0 # version of YOUR API, not of OpenAPI
description: Order lifecycle for the Checkout domain.
contact: { name: Checkout Team, email: [email protected] }
license: { name: Apache-2.0, identifier: Apache-2.0 }
servers:
- url: https://api.example.com/{basePath}
variables:
basePath: { default: v1 }
tags:
- name: Orders
description: Create and manage orders
paths:
/orders:
get:
operationId: listOrders
summary: List orders
tags: [Orders]
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema: { $ref: '#/components/schemas/OrderStatus' }
responses:
'200':
description: A page of orders
headers:
X-RateLimit-Remaining: { schema: { type: integer } }
content:
application/json:
schema: { $ref: '#/components/schemas/OrderPage' }
'401': { $ref: '#/components/responses/Unauthorized' }
'429': { $ref: '#/components/responses/TooManyRequests' }
post:
operationId: createOrder
summary: Create an order
tags: [Orders]
parameters:
- name: Idempotency-Key
in: header
required: true
schema: { type: string, maxLength: 64 }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateOrderRequest' }
examples:
basic:
value: { customerId: cus_91, items: [{ sku: SKU-1, quantity: 2 }] }
responses:
'201':
description: Created
headers:
Location: { schema: { type: string, format: uri } }
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'409': { $ref: '#/components/responses/Conflict' }
'422': { $ref: '#/components/responses/ValidationError' }
/orders/{orderId}:
parameters:
- name: orderId
in: path
required: true
schema: { type: string, pattern: '^ord_[A-Za-z0-9]{8,}$' }
get:
operationId: getOrderById
tags: [Orders]
responses:
'200':
description: The order
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
'404': { $ref: '#/components/responses/NotFound' }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
orders:read: Read orders
orders:write: Create and modify orders
parameters:
Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
Cursor: { name: cursor, in: query, schema: { type: string } }
schemas:
OrderStatus:
type: string
enum: [PENDING_PAYMENT, PAID, SHIPPED, CANCELLED]
Order:
type: object
required: [id, status, createdAt]
properties:
id: { type: string, readOnly: true, examples: [ord_8f3kd92k] }
status: { $ref: '#/components/schemas/OrderStatus' }
createdAt: { type: string, format: date-time, readOnly: true }
OrderPage:
type: object
required: [items]
properties:
items: { type: array, items: { $ref: '#/components/schemas/Order' } }
nextCursor: { type: [string, 'null'] }
responses:
NotFound:
description: Resource not found
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetail' }
security:
- bearerAuth: []

Key Objects

ObjectPurposeSenior note
operationIdUnique id; becomes SDK method nameNever auto-generate from class names; renaming it is a breaking SDK change
parameters (path/query/header/cookie)Inputs with style and explodeArrays: style: form, explode: true gives ?id=1&id=2
requestBodyPer media type schemaDocument application/json AND error application/problem+json
responsesStatus -> content/headers/linksAlways include default error and 401/403/429 where applicable
componentsReuseAnything used twice belongs here
securityGlobal/operation-level requirementssecurity: [] on an operation explicitly makes it public
x-*Vendor extensionsUseful (x-internal, x-codegen-*) but tool-specific

3. Schemas (JSON Schema 2020-12 in OAS 3.1)

Money:
type: object
required: [amount, currency]
properties:
amount: { type: string, pattern: '^-?\d+(\.\d{1,8})?$', description: Decimal as string }
currency: { type: string, pattern: '^[A-Z]{3}$', description: ISO 4217 }
additionalProperties: false
KeywordMeaningGotcha
requiredProperty MUST be presentLives on the parent object, not the property
nullable (3.0) vs type: [x, "null"] (3.1)Value may be nullNull is distinct from absent
readOnly / writeOnlyOnly in responses / only in requestsMany validators ignore; prefer separate request/response schemas
additionalProperties: falseReject unknown fieldsHurts forward compatibility for responses; fine for requests
formatHint (date-time, uuid, int64, uri)Annotation only unless validator enforces it
example / examplesDocumentation and mock data3.1 prefers examples (array)
minimum, maxLength, pattern, enumValidationPut limits in the spec so gateways can reject early
defaultServer applies if omittedNot "client sends this"

Composition & Polymorphism

Payment:
oneOf:
- $ref: '#/components/schemas/CardPayment'
- $ref: '#/components/schemas/BankTransferPayment'
discriminator:
propertyName: type
mapping:
CARD: '#/components/schemas/CardPayment'
BANK_TRANSFER: '#/components/schemas/BankTransferPayment'
CardPayment:
allOf:
- $ref: '#/components/schemas/PaymentBase'
- type: object
required: [type, last4]
properties:
type: { type: string, const: CARD }
last4: { type: string, pattern: '^\d{4}$' }
  • allOf = inheritance/mixins (all must match). oneOf = exactly one. anyOf = at least one.
  • discriminator is an optimisation/hint for code generators and validators, so they pick the subtype by the type property instead of trying every branch.
  • Pitfall: oneOf with overlapping branches (both match) is invalid - make branches mutually exclusive via const/required fields.
  • Client compatibility: adding a new subtype is a breaking change for clients with exhaustive deserialisers; document that clients must handle unknown discriminator values.

4. Security Schemes

TypeSpec fragmentUse
API keytype: apiKey, in: header, name: X-API-KeyServer-to-server identification (not strong auth)
HTTP bearertype: http, scheme: bearer, bearerFormat: JWTAccess tokens
OAuth 2.0flows: authorizationCode, clientCredentials, implicit (deprecated), password (deprecated)Scopes per operation
OpenID Connecttype: openIdConnect, openIdConnectUrl: .../.well-known/openid-configurationDiscovery
mTLS (3.1)type: mutualTLSService mesh/B2B

Apply per operation with scopes: security: [{ oauth2: [orders:write] }]. Remember: the spec documents security; it does not enforce it.

paths:
/subscriptions:
post:
callbacks:
onPaymentFailed:
'{$request.body#/callbackUrl}':
post:
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentFailedEvent' }
responses:
'204': { description: Acknowledged }
webhooks: # OAS 3.1: events not tied to a prior request
order.shipped:
post:
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/OrderShippedEvent' }
responses:
'200': { description: Acknowledged }

For event-driven/async protocols (Kafka, MQTT, WebSocket) use AsyncAPI, the sibling spec; see Reliability: Idempotency, Concurrency & Webhooks.

6. The Tooling Landscape

ToolPurposeNotes
Swagger UIInteractive docs + "Try it out"Executes real calls from the browser (CORS needed); disable or protect in production
Swagger EditorBrowser spec editor with live validationGood for learning; use IDE + linters for teams
Swagger Codegen / OpenAPI GeneratorServer stubs and client SDKs in 50+ languagesOpenAPI Generator is the more active community fork
Redoc / Scalar / Stoplight ElementsBeautiful read-only portalsBetter than Swagger UI for public docs
SpectralSpec linting with custom rulesetsCI gate
oasdiff / openapi-diffBreaking-change detectionCI gate
Prism / WireMock / MicrocksMock and validating proxyParallel development
Schemathesis / DreddContract / property-based API testingFinds 500s and schema violations
springdoc-openapi, FastAPI, NestJS Swagger, tsoaCode-first generationSee Spring Boot + springdoc

Production gotcha: exposing /swagger-ui and /v3/api-docs on public production endpoints discloses your full attack surface (hidden/internal endpoints, parameter names, schemas). Gate behind authentication, an internal network, or publish a sanitised spec (x-internal filtering) instead.

7. Common Spec Pitfalls

PitfallEffectFix
Missing required arraysGenerated clients treat everything as optional/nullableDeclare required properties explicitly
Same schema for request and responseid and createdAt show up as writableSeparate CreateX/UpdateX/X schemas
Free-form type: objectNo validation, Map<String,Object> in SDKsDefine properties or additionalProperties schema
Inline duplicated error bodiesDrift between endpointscomponents/responses
Unbounded strings/arraysDoS vectors, no gateway validationmaxLength, maxItems, maximum
Examples not matching schemaBroken mocks/docsValidate examples in CI (Spectral oas3-valid-schema-example)
Spec says 200, server returns 201Contract driftContract tests (Schemathesis)
Enum closed in responseNew value crashes clientsDocument "unknown values must be tolerated"

Next: Spring Boot + springdoc-openapi.

πŸ“–
Track Page Progress0 / 635 Read
Knowledge Base Completion0%