#OpenAPI 3.1 in an interview
AddedOpenAPI is a standard way to describe an HTTP API in a YAML or JSON file: every path, every operation on it, the parameters and bodies it takes, the responses it returns, and how callers authenticate. It started life as the Swagger Specification; Swagger 2.0 was donated to the OpenAPI Initiative at the end of 2015, and the line since then is OpenAPI 3.0.0 (July 2017), 3.1.0 (February 2021), the 3.1.1 and 3.1.2 patches (October 2024 and September 2025), 3.2.0 (September 2025) and its 3.2.1 patch (September 2026). Those dates are from the revision history in the specifications at spec.openapis.org, checked 2026-10-27. "Swagger" today is the name of SmartBear's tools (Swagger UI, Swagger Editor), not of the format.
The mental model: an OpenAPI document is the contract drawing for an API. An architect's drawing is what the builders, the inspector, the electrician and the buyer all work from; nobody pulls bricks out of the wall to find where the door is. A device API has the same crowd: the team writing the on-device server, the web UI team, partners integrating with the device, the people writing SDKs and tests, and the support engineer reading a customer's log. When they all read one file, they agree. When each reads the code, or a PDF someone last updated two releases ago, they don't.
What makes it more than documentation is that the file is data. Tools read it to render reference docs, to generate TypeScript types and client code, to validate requests and responses while the server runs, to start a mock server the UI team can build against, and to diff two versions and say which changes break existing clients. Each of those is a topic in this chapter.
openapi, info and at least one of paths, components or webhooks.What changed from 3.0 to 3.1
Interviewers who know OpenAPI often ask this, because 3.0 specs are still everywhere and the differences bite in tooling. The OpenAPI Initiative's own upgrade guide (learn.openapis.org, "Upgrading from 3.0 to 3.1", checked 2026-10-27) says the breaking changes are limited to the Schema Object; the rest are additions.
| Area | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|
| Schemas | "an extended subset" of an old JSON Schema draft (Wright-00) | a superset of JSON Schema Draft 2020-12; $schema and the root jsonSchemaDialect can name the dialect |
| Null | nullable: true next to type | nullable is gone: type: [string, "null"] |
| Exclusive bounds | minimum: 0 + exclusiveMinimum: true (a boolean) | exclusiveMinimum: 0 (a number) |
| Schema examples | example: … (one value) | examples: [ … ] (a JSON Schema array); example is deprecated |
| File uploads | type: string, format: binary | contentMediaType / contentEncoding, or an empty schema for raw bytes |
| Webhooks | not available (only per-operation callbacks) | root webhooks map |
| Required root fields | openapi, info, paths | openapi, info, and at least one of paths, components, webhooks |
info and licence | title, version, license.name and url | adds info.summary and license.identifier (an SPDX expression) |
| Reuse | no components.pathItems | components.pathItems; a Reference Object can carry its own summary and description |
| Security schemes | apiKey, http, oauth2, openIdConnect | adds mutualTLS (client certificates), useful for device-to-cloud links |
The smallest useful document
Here is the smallest useful document for a device: one operation, one webhook, two shared schemas and an auth scheme. It's valid OpenAPI 3.1 (swagger-parser accepts it); the full spec in the next topic adds what a linter would ask for.
openapi: 3.1.1
info:
title: Room Device API
version: 1.4.0
servers:
- url: https://{deviceAddress}/api/v1
variables:
deviceAddress: { default: 192.168.1.20 }
security:
- bearerAuth: []
paths:
/audio:
get:
operationId: getAudioSettings
summary: Read the audio settings.
responses:
"200":
description: Current audio settings.
content:
application/json:
schema: { $ref: "#/components/schemas/AudioSettings" }
webhooks:
deviceStatusChanged:
post:
requestBody:
content:
application/json:
schema: { $ref: "#/components/schemas/DeviceEvent" }
responses:
"204": { description: Event received. }
components:
schemas:
AudioSettings:
type: object
required: [volume, muted]
properties:
volume: { type: integer, minimum: 0, maximum: 100 }
muted: { type: boolean }
DeviceEvent:
type: object
required: [sequence, type]
properties:
sequence: { type: integer, minimum: 1 }
type: { type: string }
securitySchemes:
bearerAuth: { type: http, scheme: bearer }openapi: 3.1.1is the version of the OpenAPI Specification the file follows, which decides how tools read everything below it.info.version: 1.4.0is the version of your API. Mixing the two up is a classic interview slip: bumping your API never changes theopenapiline.serversgives the base URL, and the{deviceAddress}variable says the host is different for every device. A cloud API has one fixed server; a device API is served by thousands of devices, each at its own address, so the variable (with a default for the docs' "try it" button) is the honest description.securityat the root applies to every operation unless one overrides it.bearerAuthis defined once undercomponents.securitySchemesastype: http, scheme: bearer; the requirement- bearerAuth: []just names it.- Under
paths,/audiohas agetoperation whose200response has acontentmap keyed by media type.application/jsonpoints at a schema, here with$ref: "#/components/schemas/AudioSettings", a JSON Pointer into the same file. Every path that returns audio settings points at that one schema, so the shape is defined once. webhooksis new in 3.1: requests the device sends to a server that registered with it, described like operations. The specification describes them as requests "initiated other than by an API call". 3.0 could only describecallbackstied to a specific operation (subscribe here, we call you back at the URL you gave).components.schemasare JSON Schema. In 3.1 the Schema Object is "a superset of" JSON Schema Draft 2020-12 (the spec's wording), sotype,required,minimumandmaximummean exactly what they mean in any JSON Schema validator. That alignment is the headline change in 3.1, and the reason Ajv, the validator used later in this chapter, can check data against these schemas with no translation.
The table says nullable is gone in 3.1. What actually happens when a 3.0 habit ends up in a 3.1 file is that tools disagree, which is worse than all of them rejecting it. This model puts the same field in a 3.1 document twice, once the 3.0 way and once the 3.1 way, and asks a validator, a type generator and a linter:
// One 3.0-style schema inside a 3.1 document, read by three tools.
import Ajv2020 from "ajv/dist/2020.js";
import openapiTS, { astToString } from "openapi-typescript";
import { lintFromString, createConfig } from "@redocly/openapi-core";
import { stringify } from "yaml";
const doc = (lastError) => ({
openapi: "3.1.1",
info: { title: "Nullable test", version: "1.0.0", license: { name: "MIT", identifier: "MIT" } },
servers: [{ url: "https://device.local/api/v1" }],
paths: {},
components: { schemas: { Status: { type: "object", required: ["lastError"], properties: { lastError } } } },
});
const styles = {
"3.0 style { type: string, nullable: true }": { type: "string", nullable: true },
"3.1 style { type: [string, \"null\"] }": { type: ["string", "null"] },
};
const config = await createConfig({ extends: ["recommended"] });
for (const [label, lastError] of Object.entries(styles)) {
console.log(label);
const ajv = new Ajv2020();
const valid = ajv.compile(doc(lastError).components.schemas.Status);
console.log(` Ajv accepts { lastError: null }: ${valid({ lastError: null })}`);
const ts = astToString(await openapiTS(doc(lastError)));
console.log(` openapi-typescript: ${ts.match(/lastError: [^;]+/)[0]}`);
const problems = await lintFromString({ source: stringify(doc(lastError)), absoluteRef: "openapi.yaml", config });
const errors = problems.filter((p) => p.severity === "error").map((p) => p.message);
console.log(` Redocly lint: ${errors.join("; ") || "no errors"}`);
}3.0 style { type: string, nullable: true }
Ajv accepts { lastError: null }: true
openapi-typescript: lastError: string | null
Redocly lint: Property `nullable` is not expected here.
3.1 style { type: [string, "null"] }
Ajv accepts { lastError: null }: true
openapi-typescript: lastError: string | null
Redocly lint: no errors- Both documents say
openapi: "3.1.1"; only thelastErrorschema differs. The question each tool answers is "maylastErrorbenull?" - Ajv says yes to both. It isn't following 2020-12 here: Ajv has
nullablebuilt into its type handling (lib/compile/validate/dataType.tsadds"null"to the allowed types whennullable: true) as a convenience for OpenAPI 3.0 users. A validator that follows JSON Schema 2020-12 to the letter would treat the unknown keyword as an annotation (the core spec: "Unknown keywords SHOULD be treated as annotations") and rejectnull, becausetype: stringdoesn't allow it. - openapi-typescript also says yes to both, generating
string | null. Being lenient here means a half-migrated spec still produces sensible types. - Redocly's linter rejects the 3.0 form with "Property
nullableis not expected here." It checks the document's structure against what 3.1 allows. - So one spec can give you a type that says
nullis fine, a validator in one language that agrees, and a validator in another language (a Kotlin or Python service, a partner's gateway) that rejects every response wherelastErrorisnull. The 3.1 form,type: [string, "null"], means the same thing to all of them. That's the practical case for linting specs in CI (the next topic shows what linting catches).
OpenAPI 3.2.0 was published on 19 September 2025, and the 3.2.1 patch on 10 September 2026 (revision history at spec.openapis.org, checked 2026-10-27). From the specification text: a query operation for the HTTP QUERY method, additionalOperations for other methods, a $self field giving the document its own URI, a querystring parameter location, components.mediaTypes, and new sections on streaming and sequential media types, including Server-Sent Events, which matters for device event streams (see chapter 7).
The reason this chapter writes 3.1 is the tool chain, as installed here in October 2026:
- @apidevtools/swagger-parser 13.1.0 accepts
3.1.0,3.1.1,3.1.2and3.0.0–3.0.4and rejects anything else (supported31Versionsin itslib/index.js; the next topic shows the error). - openapi-typescript 7.13 documents support for "OpenAPI 3.0 and 3.1" (openapi-ts.dev, checked 2026-10-27).
- @redocly/openapi-core 1.34.22 reads a
3.2document with its 3.1 rules; its source says// temporary support for 3.2.
So in an interview: "3.1 is what the tooling supports end to end today; 3.2 adds things we'd want for event streams, and I'd move when our validator, generator and docs tool all support it." Check the versions again when you start a project, because this moves.
A 3.0 spec describes a retry delay that must be greater than zero as { type: integer, minimum: 0, exclusiveMinimum: true }. You change the file's first line to openapi: 3.1.1 and validate data with Ajv in JSON Schema 2020-12 mode. What happens, and what should the schema say?
Show answer
Ajv refuses to compile the schema: in 2020-12, exclusiveMinimum must be a number. The 3.1 form is { type: integer, exclusiveMinimum: 0 }.
import Ajv2020 from "ajv/dist/2020.js";
const fromOpenApi30 = { type: "integer", minimum: 0, exclusiveMinimum: true }; // "greater than 0" in 3.0
const for31 = { type: "integer", exclusiveMinimum: 0 }; // "greater than 0" in 3.1 / JSON Schema 2020-12
for (const [label, schema] of [["3.0 form", fromOpenApi30], ["3.1 form", for31]]) {
try {
const validate = new Ajv2020().compile(schema);
console.log(`${label}: 0 → ${validate(0)}, 1 → ${validate(1)}`);
} catch (e) {
console.log(`${label}: ${e.message}`);
}
}3.0 form: schema is invalid: data/exclusiveMinimum must be number
3.1 form: 0 → false, 1 → true- In OpenAPI 3.0 (based on an old JSON Schema draft),
exclusiveMinimumwas a boolean modifier ofminimum:minimum: 0plusexclusiveMinimum: truemeant "greater than 0". - JSON Schema changed the keyword to a number in later drafts, and 3.1 follows 2020-12, so
exclusiveMinimum: 0alone means "greater than 0", with nominimumneeded. - Ajv validates the schema itself against the 2020-12 meta-schema before compiling it.
trueisn't a number, so compilation fails with "schema is invalid: data/exclusiveMinimum must be number". That's the good outcome: a loud failure at startup. - The 3.1 form rejects
0and accepts1. The OpenAPI upgrade guide's checklist says exactly this: switch to the numeric form and remove the siblingminimum. - The quieter failure is
nullable(above), which some tools accept and others ignore. Changing theopenapiline is never the whole migration; lint the file with 3.1 rules afterwards.