OpenAPI and Swagger for systems analysts
Contents:
Why interviewers ask about OpenAPI
If you sit a systems analyst loop at Stripe, Linear, Airbnb, or any payments-adjacent shop, you will be asked to describe a new endpoint in OpenAPI 3.1. The interviewer is checking three things at once: do you know the format, do you know what makes a contract testable, and can you reason about backward compatibility without losing clients. The whole loop usually fits in 30 minutes and you are expected to write YAML on the whiteboard, not just talk about it.
The pain the question protects against is real. A systems analyst who writes "the endpoint accepts a JSON with user_id" in a Notion doc forces the backend engineer and the integration team to re-derive the schema, ship parallel JSON files, and discover a mismatch on the day before launch. A proper OpenAPI document collapses that into one file that lints, generates clients, and survives review.
Load-bearing trick: treat the OpenAPI file as the contract, not the docs. Everything else — Swagger UI, Postman collections, codegen — is a downstream artifact of the same source of truth.
OpenAPI vs Swagger terminology
The OpenAPI Specification (OAS) is the formal grammar for describing REST APIs. Current version is 3.1.0, released in 2021 and aligned with JSON Schema 2020-12.
Swagger is the historical name (everything up to 2.0). The project was renamed to OpenAPI in 2016 when SmartBear donated it to the Linux Foundation, but the Swagger brand stuck to the tooling:
| Tool | What it does |
|---|---|
| Swagger UI | Renders the spec as interactive HTML docs |
| Swagger Codegen | Generates client SDKs and server stubs |
| Swagger Editor | Browser-based YAML editor with live preview |
| Redoc | Alternative renderer, three-pane layout |
| Spectral | Linter, custom rulesets, CI-friendly |
The correct phrasing in 2026 is "described in OpenAPI" but "served via Swagger UI" — both are accepted.
3.1 vs 3.0 is the second half of this question. Be ready to name three differences: full JSON Schema 2020-12 compatibility (so if/then/else, $dynamicRef, unevaluatedProperties work), a new top-level webhooks block, and the removal of nullable: true in favor of type: [string, "null"]. Most interviewers do not care about 3.0 specifics anymore, but the migration story matters because half of all production APIs still live on 3.0.
Spec structure
An OpenAPI document is YAML or JSON. The root keys are stable across the 3.x line:
openapi: 3.1.0
info:
title: Orders API
version: 1.2.0
description: API for managing orders, payments, and shipments.
contact:
name: Platform team
email: platform@example.com
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging.api.example.com/v1
description: Staging
tags:
- name: orders
description: Order lifecycle
paths:
/orders: { }
/orders/{id}: { }
components:
schemas: { }
parameters: { }
responses: { }
securitySchemes: { }
security:
- bearerAuth: []The top-level security block sets the default for every operation. Operation-level security overrides it, including the empty array for public endpoints.
Paths and operations
Each path entry is an object whose keys are HTTP methods: get, post, put, patch, delete, head, options. An operation must declare its parameters, request body (where relevant), and one entry per response status code you actually return.
paths:
/orders/{id}:
get:
summary: Get an order by ID
operationId: getOrder
tags: [orders]
parameters:
- name: id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Order not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'The parameters.in field takes four values: path for variables inside the URL template, query for ?status=paid, header for request headers, and cookie for cookies. Path parameters are always required.
For POST and PUT you describe the body with requestBody:
post:
summary: Create an order
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrderRequest'
responses:
'201':
description: Created
headers:
Location:
schema: { type: string }
content:
application/json:
schema:
$ref: '#/components/schemas/Order'Every operation must have a unique operationId in kebab-case or camelCase. Codegen turns it into the method name on the generated client (client.getOrder(id)). If you skip it, Swagger Codegen falls back to path + method and you end up with ordersIdGet — readable to nobody.
Components: schemas, parameters, responses
The components section holds reusable building blocks. Anything referenced more than once should live here and be pulled in with $ref.
components:
schemas:
Order:
type: object
required: [id, status, amount]
properties:
id:
type: integer
format: int64
status:
type: string
enum: [pending, paid, shipped, cancelled]
amount:
type: number
format: decimal
items:
type: array
items:
$ref: '#/components/schemas/OrderItem'
created_at:
type: string
format: date-time
Error:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }
details: { type: object }
parameters:
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
Unauthorized:
description: Missing or invalid credentials
content:
application/json:
schema:
$ref: '#/components/schemas/Error'Reuse looks like this:
paths:
/orders:
get:
parameters:
- $ref: '#/components/parameters/LimitParam'
responses:
'401':
$ref: '#/components/responses/Unauthorized'Useful built-in formats: int32 and int64 for integers, date and date-time for timestamps, uuid, email, uri for strings, and binary or byte (base64) for file payloads. The password hint tells Swagger UI to render the input masked.
Security: bearer, apiKey, oauth2
Security schemes are declared once in components.securitySchemes and then referenced from security. The three flavors you will be asked about are bearer tokens, API keys, and OAuth2.
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKeyAuth:
type: apiKey
in: header
name: X-API-Key
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://example.com/oauth/authorize
tokenUrl: https://example.com/oauth/token
scopes:
read:orders: Read orders
write:orders: Create and modify orders
security:
- bearerAuth: []An individual operation can opt out of the default — useful for health checks and public webhooks:
paths:
/public/health:
get:
security: []Or it can require specific OAuth2 scopes:
post:
security:
- oauth2: [write:orders]Versioning and evolution
Three places to put the version, in decreasing order of popularity: in the URL (/v1/orders, the dominant pattern at Stripe, GitHub, and most B2B APIs), in a header (Accept: application/vnd.api.v1+json, common at Twilio and GitHub's GraphQL surface), and in a query parameter (/orders?version=1, generally a sign somebody lost a fight with infra).
The info.version field in the spec is the document version, not the API version — it bumps every time you publish a schema change, including non-breaking ones. Treat it as semver of the YAML file.
| Change | Type | Action |
|---|---|---|
| Add optional query param | Backward-compatible | Bump minor |
| Add new field to response | Backward-compatible | Bump minor |
| Add new endpoint | Backward-compatible | Bump minor |
| Make optional field required | Breaking | Bump major, parallel deploy |
| Remove field or enum value | Breaking | Bump major, deprecation window |
| Change a field's type | Breaking | Bump major, parallel deploy |
For breaking changes, the standard playbook is parallel deploy: ship v2 alongside v1, mark v1 operations with deprecated: true, and run a deprecation window of 6 to 12 months before turning the old paths off. Swagger UI renders deprecated operations struck through, which is usually enough nudge for client teams.
get:
deprecated: true
description: Use /v2/orders/{id} instead. v1 will be removed on 2027-01-01.Common pitfalls
The single most common interview-day mistake is not using $ref. Candidates inline the same Order schema in five operations, then explain how they would "remember to update all of them" when the field set changes. Pull every reusable object into components.schemas from the start; the linter will not catch this for you and code review usually misses it too.
The second is forgetting required. JSON Schema treats every property as optional by default, so a schema that lists ten fields without a required array is documenting that the server might return any subset. Production fallout is real: backend nulls become client undefined, and a serialization bug masquerades as a business logic issue for a week.
A close third is skipping error schemas. The document describes the happy path with a single 200 response, and the integration team has to grep server code to learn that validation failures return 422 with a details array, while rate limiting returns 429 with a Retry-After header. Every operation should list every status code it actually returns, and the error body should be a reusable schema.
Generic typing is the elegant version of the same mistake — declaring payload: object because you do not want to commit to a structure. Codegen produces Map<String, Object> clients, Swagger UI shows an empty box, and consumers send whatever they want. If the payload is genuinely polymorphic, use oneOf with a discriminator; if it is opaque (an analytics blob, a third-party callback), at least add an additionalProperties schema and an example.
URL-only versioning without a deprecation policy is the way v1 lives forever. Clients keep pointing at it, and the team that owns v2 cannot delete the old code because nobody knows who still depends on it. Pair every URL version with a published end-of-life date and a metric on the User-Agent header so you can see who is still on the old version.
Finally, confidential data in description keeps catching new SAs. The spec ships to Swagger UI on a public docs site, and lines like "use the test key XXX-YYY" end up indexed by Google within a week. Treat the spec like any other published artifact — secrets do not belong in it.
Related reading
- HTTP methods and status codes for SA interviews
- Idempotency keys — systems analyst interview
- API gateway vs BFF — systems analyst interview
- API contract testing — systems analyst interview
- ETag headers — systems analyst interview
If you want to drill SA contract questions like this every day, NAILDD is launching with hundreds of systems analyst problems built around exactly this pattern.
FAQ
Do I write the spec by hand or generate it from code?
Two camps. Design-first means you author the OpenAPI YAML before any code, review it with backend and frontend together, and only then implement. Code-first generates the spec from annotations on controllers (Spring's springdoc, FastAPI, NestJS). Design-first scales better in teams with a real systems analyst because the contract review happens upstream of implementation; code-first is fine for backend-only teams with no SA in the loop. In interviews, expect design-first — the question is usually "open a YAML file and describe this endpoint", not "annotate this Java method".
How do I test an API against its OpenAPI spec?
The standard tools are Dredd and Schemathesis, both of which read the spec and generate requests to validate that the server actually conforms. Schemathesis adds property-based fuzzing on top, which catches edge cases like empty arrays and oversized strings that hand-written tests usually miss. For collection-style testing, Postman imports OpenAPI documents and turns each operation into a request; pair it with Newman in CI for smoke tests on every deploy.
Does OpenAPI describe GraphQL or gRPC?
No. OpenAPI is REST-only. GraphQL uses SDL (Schema Definition Language) introspected from the server. gRPC uses Protocol Buffers (.proto files) and the schema is the source of truth for both client and server generation. A reasonable cross-protocol comparison is part of most senior SA loops, so be ready to sketch the differences and explain which problem each format solves best.
What is discriminator in OAS?
A discriminator is the field whose value tells the parser which schema in a oneOf or anyOf is actually being used. Without it, the parser has to try each schema in turn and pick the first that validates — slow and ambiguous. With it, the parser reads the discriminator field and jumps directly to the correct schema.
Pet:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: pet_type
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'Can OpenAPI describe webhooks?
In 3.1 yes, through the top-level webhooks block — it mirrors the paths structure but flips the direction, declaring that the server will call out to a URL the client owns. In 3.0 the closest equivalent is callbacks nested inside a specific operation, which is awkward when the webhook is unrelated to any single API call. If your team is on 3.0 and ships webhooks, the migration to 3.1 is usually worth a quarter of cleanup time.
Is everything in this article authoritative?
No. The article is based on the OpenAPI 3.1 specification (spec.openapis.org/oas/v3.1.0) and Swagger documentation. Tooling behavior changes; always check the version notes for Spectral, Schemathesis, and your codegen of choice before relying on a specific feature in production.