Learn/Practical Guides

OpenAPI & JSON — Contract-First API Design

OpenAPI is the industry standard for describing REST APIs. Built on JSON Schema, it lets you define your API contract once and generate documentation, client SDKs, server stubs, and contract tests automatically. This guide covers OpenAPI 3.1, schema design, code generation, and contract testing workflows.

What OpenAPI Is and Why It Matters

Contract-First Workflow
Without OpenAPIWith OpenAPI
Documentation written manually, drifts from codeDocs auto-generated from single source of truth
Client types written by hand, break on API changesTypeScript/Java types generated from spec
Integration tests catch schema mismatches lateContract tests validate every request/response
API changes break clients without warningBreaking changes detected by spec diff tools
Mock servers require custom implementationMock server generated from spec with realistic data

OpenAPI 3.1 and JSON Schema Alignment

OpenAPI 3.1 fully adopts JSON Schema Draft 2020-12. This means every schema keyword you know from JSON Schema works directly in your OpenAPI spec:

FeatureOpenAPI 3.0OpenAPI 3.1
JSON Schema alignmentPartial (extended subset)Full Draft 2020-12
nullablenullable: true keywordtype: ["string", "null"] (standard JSON Schema)
if/then/elseNot supportedFully supported
$dynamicRefNot supportedSupported
examplesSingle example per schemaexamples array (JSON Schema standard)
Content typesLimitedSupports webhooks, JSON/YAML/binary

Anatomy of an OpenAPI Spec

Minimal OpenAPI 3.1 specificationyaml
1openapi: 3.1.0
2info:
3 title: User API
4 version: 1.0.0
5 description: Manage user accounts
6
7servers:
8 - url: https://api.example.com/v1
9 description: Production
10
11paths:
12 /users:
13 get:
14 operationId: listUsers
15 summary: List all users
16 parameters:
17 - name: page
18 in: query
19 schema:
20 type: integer
21 default: 1
22 - name: per_page
23 in: query
24 schema:
25 type: integer
26 default: 20
27 maximum: 100
28 responses:
29 "200":
30 description: A paginated list of users
31 content:
32 application/json:
33 schema:
34 $ref: "#/components/schemas/UserListResponse"
35
36 post:
37 operationId: createUser
38 summary: Create a new user
39 requestBody:
40 required: true
41 content:
42 application/json:
43 schema:
44 $ref: "#/components/schemas/CreateUserRequest"
45 responses:
46 "201":
47 description: User created
48 content:
49 application/json:
50 schema:
51 $ref: "#/components/schemas/User"
52 "422":
53 description: Validation error
54 content:
55 application/json:
56 schema:
57 $ref: "#/components/schemas/ErrorResponse"
58
59components:
60 schemas:
61 User:
62 type: object
63 required: [id, name, email]
64 properties:
65 id:
66 type: string
67 format: uuid
68 name:
69 type: string
70 minLength: 1
71 email:
72 type: string
73 format: email
74 plan:
75 type: string
76 enum: [free, pro, enterprise]
77 default: free
78 created_at:
79 type: string
80 format: date-time
81
82 CreateUserRequest:
83 type: object
84 required: [name, email]
85 properties:
86 name:
87 type: string
88 minLength: 1
89 maxLength: 100
90 email:
91 type: string
92 format: email
93 plan:
94 type: string
95 enum: [free, pro, enterprise]
96
97 UserListResponse:
98 type: object
99 properties:
100 data:
101 type: array
102 items:
103 $ref: "#/components/schemas/User"
104 total:
105 type: integer
106 page:
107 type: integer
108 per_page:
109 type: integer
110
111 ErrorResponse:
112 type: object
113 properties:
114 error:
115 type: string
116 details:
117 type: array
118 items:
119 type: object
120 properties:
121 field:
122 type: string
123 message:
124 type: string

Polymorphism: oneOf, anyOf, discriminator

Discriminated union with discriminatoryaml
1components:
2 schemas:
3 Event:
4 oneOf:
5 - $ref: "#/components/schemas/UserCreated"
6 - $ref: "#/components/schemas/OrderPlaced"
7 discriminator:
8 propertyName: event_type
9 mapping:
10 user.created: "#/components/schemas/UserCreated"
11 order.placed: "#/components/schemas/OrderPlaced"
12
13 UserCreated:
14 type: object
15 required: [event_type, user_id, email]
16 properties:
17 event_type:
18 type: string
19 const: user.created
20 user_id:
21 type: string
22 email:
23 type: string
24 format: email
25
26 OrderPlaced:
27 type: object
28 required: [event_type, order_id, total]
29 properties:
30 event_type:
31 type: string
32 const: order.placed
33 order_id:
34 type: string
35 total:
36 type: number

When to Use discriminator

Use discriminator when your API returns different shapes based on a type field. Code generators use it to produce proper tagged unions (TypeScript discriminated unions, Java sealed classes, C# pattern matching).

Code Generation

TypeScript Types

Generate TypeScript types from OpenAPIbash
1# Install openapi-typescript
2npm install -D openapi-typescript
3
4# Generate types from a local spec
5npx openapi-typescript ./openapi.yaml -o ./src/api-types.ts
6
7# Generate from a remote URL
8npx openapi-typescript https://api.example.com/openapi.json -o ./src/api-types.ts
Generated types (example output)typescript
1// Auto-generated from openapi.yaml
2export interface components {
3 schemas: {
4 User: {
5 id: string;
6 name: string;
7 email: string;
8 plan?: "free" | "pro" | "enterprise";
9 created_at?: string;
10 };
11 CreateUserRequest: {
12 name: string;
13 email: string;
14 plan?: "free" | "pro" | "enterprise";
15 };
16 };
17}
18
19// Use in your fetch calls with full type safety
20type User = components["schemas"]["User"];

Other Languages

LanguageToolCommand
TypeScriptopenapi-typescriptnpx openapi-typescript spec.yaml -o types.ts
Javaopenapi-generatornpx @openapitools/openapi-generator-cli generate -i spec.yaml -g java
C#NSwagnswag openapi2csclient /input:spec.yaml /output:Client.cs
Pythonopenapi-python-clientopenapi-python-client generate --path spec.yaml
Gooapi-codegenoapi-codegen -generate types spec.yaml > types.go

Contract Testing

Mock Server with Prism

Run a mock server from your specbash
1# Install Prism
2npm install -g @stoplight/prism-cli
3
4# Start mock server on port 4010
5prism mock openapi.yaml
6
7# Test against the mock
8curl http://localhost:4010/users
9# Returns realistic sample data based on your schema

Validation Proxy

Validate real API responses against the specbash
1# Run Prism as a validation proxy in front of your real API
2prism proxy openapi.yaml http://localhost:3000
3
4# Send requests through the proxy (port 4010)
5curl http://localhost:4010/users
6
7# Prism validates:
8# - Request parameters match the spec
9# - Request body matches the schema
10# - Response status code is defined in the spec
11# - Response body matches the schema
12# Violations are logged with details

Automated Testing with Schemathesis

Property-based API testingbash
1# Install Schemathesis
2pip install schemathesis
3
4# Run automated tests against your API
5schemathesis run http://localhost:3000/openapi.json
6
7# Schemathesis generates hundreds of valid and edge-case
8# requests based on your schema, testing for:
9# - 500 errors on valid inputs
10# - Schema violations in responses
11# - Content-type mismatches
12# - Undocumented status codes

Documentation Generation

ToolStyleFeatures
Swagger UIInteractiveTry-it-out console, parameter forms, built-in auth
RedocThree-panelClean read-only docs, nested schema display, search
StoplightFull platformVisual editor, hosted docs, style guides, mock server
ScalarModernBeautiful UI, dark mode, code samples in 20+ languages

Versioning Your API Spec

Change TypeExampleBreaking?Strategy
Add optional fieldNew "avatar_url" propertyNoSafe to add without version bump
Add new endpointPOST /users/inviteNoSafe to add without version bump
Remove a fieldDrop "legacy_id" propertyYesDeprecate first, remove in next major
Change field typeprice: string -> numberYesNew major version or new field name
Make optional field requiredemail becomes requiredYesNew major version
Rename an endpoint/users -> /accountsYesKeep old endpoint, add redirect

Note

Use tools like oasdiff or optic to automatically detect breaking changes in CI. Run them on pull requests to prevent accidental contract breakage.

Frequently Asked Questions

What is OpenAPI?
OpenAPI (formerly Swagger) is a specification for describing REST APIs as a machine-readable JSON or YAML document. It defines endpoints, request/response schemas, authentication, and error formats. The specification is maintained by the OpenAPI Initiative and is the industry standard for API documentation and contract design.
How does OpenAPI relate to JSON Schema?
OpenAPI 3.1 fully aligns with JSON Schema Draft 2020-12. This means every schema in an OpenAPI document is a valid JSON Schema, and you can use all JSON Schema keywords (if/then/else, $dynamicRef, etc.) directly. Earlier versions (3.0 and below) used a subset of JSON Schema with some incompatibilities.
What is contract-first API design?
Contract-first means you write the OpenAPI specification before writing any code. The spec becomes the source of truth: server stubs, client SDKs, documentation, and tests are all generated from it. This ensures consistency between what the API promises and what it delivers.
Can I generate TypeScript types from an OpenAPI spec?
Yes. Tools like openapi-typescript generate TypeScript types directly from an OpenAPI 3.x spec. The generated types include all request parameters, response bodies, and error shapes, giving you end-to-end type safety from spec to client code.
What is Prism and how does it help with contract testing?
Prism (by Stoplight) is a mock server and validation proxy that reads your OpenAPI spec. As a mock server, it returns realistic responses based on schema examples. As a validation proxy, it sits between client and server and flags any request or response that violates the spec.
Should I write OpenAPI specs in JSON or YAML?
YAML is the most common authoring format because it is more readable and supports comments. JSON is used for programmatic consumption and storage. Most OpenAPI tools accept both. Write in YAML, distribute in JSON if needed.