OpenAPI 3.1 spec examples: endpoints, schemas, auth, and validation
Quick answer
💡An OpenAPI 3.1 spec requires four top-level keys: openapi (the version string), info (title and version), paths (the endpoint map), and optionally components for reusable schemas. Write it in YAML for readability. Use $ref: '#/components/schemas/User' to reuse schema definitions across multiple endpoints. Validate the spec with spectral lint before publishing, and use /tools/http-request-builder to test the actual endpoints against the spec.
Error symptoms
- ✕
Validator reports Missing required property: openapi when using the Swagger 2.0 swagger key - ✕
requestBody not recognized because the spec uses the Swagger 2.0 body parameter style - ✕
dollar-ref resolution fails because the target schema path contains a typo - ✕
Code generation produces incorrect types because response schemas are undefined - ✕
Spectral lint reports off-by-default rules that are not obvious from the OpenAPI docs - ✕
Security scheme is defined but not applied, so generated clients do not send auth headers
Common causes
- •Using Swagger 2.0 syntax with an openapi: 3.0 or 3.1 version field
- •Defining security schemes in components/securitySchemes but not applying them at path or global level
- •Omitting the content key from requestBody, which is required in OpenAPI 3.x
- •Using allOf to merge schemas when oneOf or anyOf is semantically correct for polymorphic responses
- •Missing the required array in object schemas, which makes all properties optional in code generation
- •Referencing a $ref that points to a component that does not exist, causing silent omission in some validators
When it happens
- •Writing a spec from scratch without a template or validation step
- •Migrating a Swagger 2.0 spec to OpenAPI 3.0 or 3.1
- •Adding authentication to an existing spec for the first time
- •Using code generation tools that produce empty or incorrect client types
- •Publishing API documentation that renders incorrectly in Redoc or Swagger UI
Examples and fixes
YAML spec showing required fields, requestBody, and 201 response with schema ref.
Minimal valid OpenAPI 3.1 spec with a POST endpoint
❌ Wrong
# Swagger 2.0 format — invalid as OpenAPI 3.x
swagger: "2.0"
info:
title: My API
basePath: /v1
paths:
/users:
post:
parameters:
- in: body
name: body
schema:
type: object
responses:
200:
description: Created✅ Fixed
# Valid OpenAPI 3.1.0 spec
openapi: "3.1.0"
info:
title: User API
version: "1.0.0"
paths:
/v1/users:
post:
operationId: createUser
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: User created successfully
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"422":
$ref: "#/components/responses/ValidationError"
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
CreateUserRequest:
type: object
required: [name, email]
properties:
name:
type: string
email:
type: string
format: email
responses:
ValidationError:
description: Validation failed
content:
application/json:
schema:
type: object
properties:
error:
type: object
properties:
code:
type: string
message:
type: string
details:
type: arrayThe broken spec uses swagger: 2.0 syntax with a body parameter in the parameters array, which is the Swagger 2.0 style. OpenAPI 3.x moved request body to a dedicated requestBody field at the operation level. The basePath field was replaced by a servers array. Status codes must be quoted strings in YAML to avoid YAML parsing them as integers. The fixed spec uses operationId for code generation, defines required arrays so code generators produce non-optional types, and reuses error responses via dollar-ref. The Location header is defined in the 201 response so code generators can surface it to clients.
Bearer JWT auth applied globally and oneOf for a response that returns different shapes.
Security schemes and oneOf for polymorphic responses
❌ Wrong
# Security scheme defined but not applied — clients never send auth
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/v1/orders:
get:
responses:
"200":
description: Order list
# bearerAuth is defined but no security field applies it
# allOf used for polymorphic response (wrong semantics)
OrderResponse:
allOf:
- $ref: "#/components/schemas/PhysicalOrder"
- $ref: "#/components/schemas/DigitalOrder"✅ Fixed
# Apply security globally; override per-endpoint for public routes
openapi: "3.1.0"
info:
title: Orders API
version: "1.0.0"
security:
- bearerAuth: []
paths:
/v1/orders:
get:
operationId: listOrders
summary: List orders for authenticated user
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, shipped, delivered]
- name: cursor
in: query
schema:
type: string
responses:
"200":
description: Paginated order list
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/OrderResponse"
nextCursor:
type: string
"401":
$ref: "#/components/responses/Unauthorized"
/v1/health:
get:
operationId: healthCheck
security: [] # Override: public endpoint, no auth
responses:
"200":
description: Service healthy
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
OrderResponse:
oneOf:
- $ref: "#/components/schemas/PhysicalOrder"
- $ref: "#/components/schemas/DigitalOrder"
discriminator:
propertyName: type
PhysicalOrder:
type: object
required: [id, type, shippingAddress]
properties:
id:
type: string
type:
type: string
enum: [physical]
shippingAddress:
type: string
DigitalOrder:
type: object
required: [id, type, downloadUrl]
properties:
id:
type: string
type:
type: string
enum: [digital]
downloadUrl:
type: stringDefining a security scheme in components/securitySchemes without a security field at either the global or operation level produces a spec that validates but generates clients that never send the Authorization header. Adding security: [{bearerAuth: []}] at the top level applies the scheme to every operation. Individual public endpoints override it with security: [] to explicitly opt out. The oneOf discriminator pattern is semantically correct for a response that can be either a PhysicalOrder or a DigitalOrder. allOf is for schema composition (combining properties), not for expressing mutually exclusive types. Using allOf here would tell code generators the response has all properties from both schemas simultaneously, which is incorrect.
OpenAPI versions and what changed in 3.1
Three distinct versions of the OpenAPI specification are in active use, and they are not backward-compatible. Swagger 2.0, released in 2014, uses the swagger key and organizes request bodies as parameters with in: body. OpenAPI 3.0.x, released in 2017, introduced the openapi key, replaced in-body parameters with the dedicated requestBody field, moved servers out of basePath, and reorganized reusable definitions under components. OpenAPI 3.1.0, released in 2021, aligned the schema dialect with full JSON Schema draft 2020-12, which means features like unevaluatedProperties and if/then/else are now valid.
The most common source of confusion is mixing version-specific syntax. A developer who writes openapi: 3.0.0 at the top and then writes parameters with in: body below it produces a spec that fails validation but may pass some lenient parsers. Swagger UI sometimes renders both syntaxes without flagging the inconsistency, masking the problem until a code generation tool or strict validator rejects the spec.
YAML and JSON are both valid formats for OpenAPI specs. YAML is preferred by most teams because it is easier to read and write by hand, supports comments, and avoids the visual noise of JSON curly braces and quotes. JSON is easier to generate programmatically and parse with standard libraries. A significant YAML gotcha is unquoted status codes: writing 200: instead of "200": causes YAML to parse the key as an integer, which some parsers reject because OpenAPI requires status codes to be strings.
The required field in object schemas is not optional. Without it, all properties are treated as optional by code generators. A User schema with properties for name and email but no required: [name, email] produces TypeScript types where both fields are optional, which does not match the server behavior. Always define required for every schema that has mandatory properties.
OpenAPI 3.1 is still not universally supported. Redoc added 3.1 support in version 2.0. Swagger UI has partial support. Many code generation tools, including openapi-generator-cli, have mixed support for 3.1-specific features. If maximum toolchain compatibility is required, OpenAPI 3.0.3 remains the safer choice until tooling catches up.
Validating and linting your spec
Two tools cover different levels of spec correctness. swagger-cli validate checks structural validity: required fields, correct types, and resolvable dollar-ref references. It catches most syntactic errors. Spectral lint applies rule-based checks that go beyond syntax into API design quality: missing operationId, undefined response schemas, inconsistent naming conventions, and missing examples.
Run swagger-cli validate openapi.yaml first. If it reports errors, fix them before running Spectral because Spectral errors are harder to interpret when the spec has structural problems underneath. The swagger-cli tool is available via npm: npm install -g @apidevtools/swagger-cli.
Install Spectral with npm install -g @stoplight/spectral-cli and create a .spectral.yaml configuration file at the repository root. The minimal configuration that catches common problems looks like: extends: ["spectral:oas"] with selected rules enabled. The oas ruleset catches missing operationId, undefined response content, and dollar-ref resolution failures.
For finding dollar-ref errors, swagger-cli bundle openapi.yaml --outfile bundled.yaml is useful. It resolves all references into a single file. If any reference fails to resolve, the bundle step fails with an explicit error pointing to the broken path. This is faster than reading Spectral output for large specs.
For code generation validation, run openapi-generator-cli generate and inspect the generated types. Missing required arrays show up as optional types. Missing response schemas show up as any or object types. Missing operationId values cause code generators to invent method names based on the path and method, which are often verbose and inconsistent.
Test the actual endpoints described in the spec using /tools/http-request-builder to verify that server behavior matches the spec. A spec that validates correctly but does not match the server's actual behavior will produce generated clients that fail at runtime. Spec-first development closes this gap by using the spec to generate server stubs, ensuring alignment by construction.
Writing reusable components with dollar-ref
The dollar-ref mechanism is the most powerful feature for keeping large OpenAPI specs maintainable. Without dollar-ref, every endpoint must inline the full schema for every request and response. With dollar-ref, a User schema defined once in components/schemas/User can be referenced from every operation that returns a user, from list endpoints that return arrays of users, and from create and update endpoints that accept user data.
A dollar-ref must be an exact JSON Pointer to a definition within the spec or an external file. The local reference format is $ref: "#/components/schemas/User" where the leading hash means the root of the current document. The path segments are separated by forward slashes. A typo in any segment silently omits the referenced schema in some tools or produces an error in strict validators. Use the swagger-cli bundle step to catch broken references early.
Components has several subsections. components/schemas holds data model definitions. components/responses holds reusable response definitions including status code, headers, and content. components/parameters holds reusable query, path, and header parameters. components/examples holds example values for documentation. components/securitySchemes holds authentication definitions.
For security schemes, three types cover most cases. Bearer JWT uses type: http, scheme: bearer, bearerFormat: JWT. API key in the Authorization header uses type: apiKey, in: header, name: X-API-Key. OAuth2 authorization code uses type: oauth2 with a flows.authorizationCode field containing authorizationUrl and tokenUrl. Define the scheme once in components/securitySchemes and apply it globally with a top-level security field. Override it per-operation to mark public endpoints with security: [].
For polymorphic responses where the shape depends on a discriminator field, use oneOf with a discriminator key. The discriminator specifies which property in the response object determines the concrete type. Code generators use this information to produce union types in TypeScript and sealed classes in Kotlin. allOf is for merging schema definitions, not for expressing exclusive type alternatives. Using allOf for polymorphism generates incorrect types that combine all properties from all variants.
For validation errors, define a single ValidationError component and reference it from every 422 response. Keeping the error schema in one place ensures the error format is consistent across all endpoints, which simplifies client error handling.
Nullable, readOnly, and deprecated fields
The nullable field behaves differently between OpenAPI 3.0.x and 3.1. In OpenAPI 3.0.x, nullable: true is a non-standard extension that indicates a property can be null. In OpenAPI 3.1, which aligns with JSON Schema, the correct way to express nullability is type: [string, null] using a type array. Tools that validate against 3.0 will reject the array form, and tools that validate against 3.1 will report a warning for nullable: true as a deprecated non-standard extension.
readOnly and writeOnly properties allow you to define properties that appear only in responses or only in requests respectively. A User schema with a createdAt property that is readOnly: true indicates the field should not be included in POST or PUT request bodies. Code generators honor this flag by omitting readOnly fields from create and update request types.
The deprecated: true flag marks operations and schemas as deprecated without removing them. Swagger UI and Redoc render deprecated operations with a strikethrough style. Code generation tools may annotate generated methods with language-specific deprecation annotations. Marking an endpoint as deprecated in the spec is the correct way to signal to API consumers that they should migrate, without breaking existing integrations immediately.
Example objects in schemas help both documentation and contract testing. A schema property can include an example value that Swagger UI and Redoc display in the documentation. The examples field in a request or response content object allows multiple named examples. Mock server tools like Prism use these examples to generate realistic mock responses.
When using external dollar-refs that point to separate files, the path must be resolvable at build time. $ref: "./schemas/user.yaml#/User" works when user.yaml is in the same directory. The swagger-cli bundle command resolves all external references into a single inline file, which simplifies distribution and tool compatibility. Code generation tools often require a single-file spec or a bundled spec rather than a multi-file spec.
Spec errors that break code generation
Omitting operationId from operations is the most impactful mistake for code generation. Without operationId, generators invent names based on the HTTP method and path. GET /v1/users/orders/items becomes something like getV1UsersOrdersItems, which is verbose and unpredictable. Adding operationId: listOrderItems gives generators a clean, intentional name that appears in the generated SDK.
Missing the required array in object schemas makes all properties optional in generated TypeScript interfaces and Python dataclasses. A User with required: [id, name, email] generates TypeScript as { id: number; name: string; email: string }. Without the required array, it generates as { id?: number; name?: string; email?: string }, which requires callers to handle undefined for fields that the API guarantees are always present.
Using string response descriptions without content schemas tells code generators nothing about the response shape. The responses field in an operation should always include a content key with an application/json media type and a schema. Without it, code generators produce response types of void or any, making the SDK useless for type-safe consumption.
Defining a security scheme without applying it is a common oversight. The scheme definition in components/securitySchemes only registers the authentication method. Adding the global security field or per-operation security fields is what actually tells clients and code generators to include the Authorization header.
Forgetting to validate the spec before publishing means errors accumulate and become harder to fix. Each breaking fix to the spec after external clients have generated code from it requires those clients to regenerate. A validation step in CI, running swagger-cli validate and spectral lint on every pull request, catches errors before they reach external consumers.
Spec-first development and documentation workflow
Spec-first development inverts the typical workflow. Instead of writing server code and then documenting it, you write the OpenAPI spec first, validate it, and then use the spec to generate both server stubs and client SDKs. This approach guarantees that the spec is always accurate because the server is implemented against it by construction.
Keeep the spec in version control alongside the server code. When a developer changes an endpoint, they must also update the spec as part of the same pull request. A CI check that validates the spec prevents the spec from drifting from the implementation. Some teams run spectral lint as a required check so that design rule violations block merge.
Use the servers array to document multiple environments. Add a servers entry for production, staging, and local development. Swagger UI and Redoc display a server selector that lets developers switch between environments when testing from the documentation. This eliminates the need for separate documentation per environment.
Generate human-readable documentation automatically from the spec. Redoc produces clean, three-panel documentation with navigation, schema descriptions, and request examples. Swagger UI provides an interactive sandbox where developers can make real API calls from the browser. Both tools take the spec file as input and render without any custom templates.
For TypeScript projects, ts-node-codegen and openapi-typescript generate type-safe interfaces directly from the spec. These interfaces can be imported by both the server and client, ensuring they use the same type definitions. When the spec changes, regenerating the types surfaces breaking changes at compile time rather than at runtime.
Test endpoints with /tools/http-request-builder to verify that actual server behavior matches the spec. Spec validation checks the document structure but cannot verify that the server produces the documented response shapes. Regular manual spot-checks or automated contract tests using a tool like Dredd close this gap.
OpenAPI spec review checklist
- ✓Top-level openapi key is 3.0.x or 3.1.0, not swagger: 2.0.
- ✓Every operation has a unique operationId for clean code generation.
- ✓All request bodies use requestBody with a content key, not in: body parameters.
- ✓Every object schema defines a required array listing mandatory properties.
- ✓Security scheme is both defined in components/securitySchemes and applied in the security field.
- ✓Run swagger-cli validate to check structural correctness and all dollar-ref paths.
- ✓Run spectral lint with the oas ruleset to catch design-level issues.
- ✓Test actual endpoint behavior against the spec using /tools/http-request-builder.
Related guides
Frequently asked questions
What is the difference between OpenAPI 3.0 and OpenAPI 3.1?
OpenAPI 3.1 aligns the schema dialect with full JSON Schema draft 2020-12, enabling features like type arrays for nullable fields (type: [string, null]) and if/then/else conditionals. OpenAPI 3.0 used nullable: true as a non-standard extension. 3.1 also allows webhooks and other improvements. Tooling support for 3.1 is still catching up, so 3.0.3 is safer if broad tool compatibility is required.
Is YAML or JSON better for writing OpenAPI specs?
YAML is better for manual authoring because it is more readable, supports inline comments, and requires less punctuation. JSON is better for programmatic generation because it is a strict subset of JavaScript and easier to produce with standard serializers. Either format is valid and can be converted to the other. One important YAML gotcha: status code keys must be quoted strings like 200, not bare integers.
How do I apply authentication to all endpoints in OpenAPI?
Define the scheme once in components/securitySchemes, then add a global security field at the top level of the spec. This applies the scheme to every operation. To mark individual public operations as unauthenticated, add security: [] at the operation level to override the global setting. Defining the scheme without the security field means clients will never send authentication.
What is the difference between oneOf, anyOf, and allOf?
allOf is for schema composition: a combined schema that must satisfy all referenced schemas simultaneously. oneOf is for mutually exclusive types: the value must match exactly one of the listed schemas. anyOf is more permissive: the value must match at least one. Use oneOf with a discriminator for polymorphic responses where a type field determines the concrete shape. Use allOf when adding properties to a base schema.
How do I validate my OpenAPI spec?
Run swagger-cli validate openapi.yaml to check structural correctness including required fields and resolvable dollar-ref paths. Then run spectral lint openapi.yaml with the spectral:oas ruleset to check design quality rules like missing operationId and undefined response schemas. Both tools are available via npm. Add these checks to CI so that spec errors are caught on every pull request.
What should I put in components/schemas?
Any data model definition that appears in more than one place in the spec. User, Order, Product, and ErrorResponse are common examples. Request body schemas and response body schemas that are shared across multiple operations should always be extracted to components/schemas and referenced with dollar-ref. This keeps the spec DRY and ensures that when a schema changes, all references update automatically.
Why does my code generator produce optional fields even for required properties?
The required array in the schema definition is missing or the property name is not listed in it. In OpenAPI, all properties are optional by default. The required array at the object schema level lists which properties must be present. If required: [id, name] is defined, code generators produce non-optional fields for those properties. Add required explicitly to every object schema that has mandatory properties.
How do I document cursor-based pagination in OpenAPI?
Add a cursor query parameter to the operation using parameters with in: query, then include a nextCursor field in the response schema. Mark cursor as not required since it is absent on the first request. In the response schema, nextCursor should be nullable or optional to indicate it is absent on the last page. Document the expected behavior in the operation description so client developers understand the pagination contract.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.