Swagger 2.0 to OpenAPI 3.x migration: a field-tested checklist
A surprising number of production APIs still publish Swagger 2.0 documents. Some are frozen services nobody dares touch; more often the spec is generated by an old plugin — springfox instead of springdoc, an unmaintained flask-restplus, a swagger-node-express setup — and the document has simply never been converted. The automatic migration tools work well enough to feel safe, which is exactly when teams ship a converted spec that misdescribes nullability, bodies, and auth. This is the checklist we use to get a 2.0 document all the way to OpenAPI 3.2 with the semantics intact.
Step 0: decide the target
Do not migrate to 3.0.3 in 2026. If you are touching the document, target 3.2 (or 3.1 if a specific downstream tool has not caught up — check your renderer, code generator, and gateway first). The 3.0 stop adds a second migration later, and the nullable keyword it requires is already a legacy construct you would immediately have to remove.
Step 1: take an inventory before converting
Run through the source document and list the features that convert badly:
- Every
consumes/producesdeclaration and where they differ per operation. - Every
bodyandformDataparameter (these become request bodies and are the biggest structural change). - Every
nullable: x-nullablevendor extension and every field that can actually be null in responses. securityDefinitionstypes, especiallybasicandapiKeylocations.collectionFormaton array parameters.- Global
definitionswith circular or polymorphic refs (discriminator). - Example payloads kept in wikis or tests, because converted examples are where drift hides.
Ten minutes of inventory saves a day of "the generated SDK does not match reality."
Step 2: run the mechanical conversion
The standard tool is swagger2openapi (maintained as swagger2openapi / the online editor converters). Run it against the file and keep both versions:
npx swagger2openapi --outfile openapi-3.yaml swagger-2.jsonIt handles the bulk renames: swagger: "2.0" becomes openapi: 3.x, definitions moves under components.schemas, securityDefinitions under components.securitySchemes, and parameters reshapes. Treat the output as a draft. The rest of this checklist is the review.
Step 3: request bodies are the number-one break
In 2.0, a request body was just another parameter with in: body. In 3.x it is a dedicated requestBody object keyed by media type:
# 2.0
parameters:
- in: body
name: body
required: true
schema:
$ref: '#/definitions/ProjectCreate'
# 3.2
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProjectCreate'Watch for three conversion failures: formData parameters that must become multipart/form-data or application/x-www-form-urlencoded content (converters sometimes emit an empty JSON body instead), file uploads (type: file becomes a binary string schema under the right media type), and per-operation consumes that override the global default — each needs its own content map.
Step 4: responses get content too
2.0 responses had a direct schema. In 3.x that schema lives under content.<media type>:
responses:
'200':
description: A project
content:
application/json:
schema:
$ref: '#/components/schemas/Project'If the API serves both JSON and CSV, this is where you finally document both properly — the old produces array flattened that distinction.
Step 5: nullability done right
2.0 had no null type; teams used the vendor extension x-nullable, or nothing at all. The 3.0 converter emits nullable: true. In 3.2 you must convert again to the JSON Schema form:
# converted to 3.0 style (do not keep)
refundedAt:
type: string
nullable: true
# 3.2 target
refundedAt:
type: [string, "null"]
format: date-timeDo not stop at find-and-replace. Check the actual API behavior against logs: fields that are omitted are not the same as fields returned as null, and optional-but-non-null is different from nullable. This is the single most common semantic lie in migrated specs, and it generates wrong SDK types (pointer vs union vs value).
Step 6: security schemes
# 2.0
securityDefinitions:
apiKey:
type: apiKey
in: header
name: X-API-Key
appAuth:
type: basic
# 3.2
securitySchemes:
apiKey:
type: apiKey
in: header
name: X-API-Key
appAuth:
type: http
scheme: basictype: basic becomes type: http, scheme: basic; OAuth2 flows restructure into a flows object with explicit authorization/token/refresh URLs per flow; in: query API keys convert mechanically but should trigger a security review (query-string secrets leak into logs).
Step 7: parameters and arrays
collectionFormat: csv|ssv|tsv|pipesbecomesstyle: form|spaceDelimited|pipeDelimitedwithexplode. Multi-valued query parameters are a frequent source of client bugs; verify the generated client actually serializes the way the server expects.- Path parameters now require
required: trueexplicitly and validators enforce it. exclusiveMinimum: true(a boolean in 2.0-era JSON Schema dialect) becomes a numeric value under draft 2020-12.
Step 8: examples and descriptions
2.0 allowed a single example on schemas; 3.x adds a media-type-level examples map (named examples with summaries). Migration is the moment to replace the classic "string" and 123 placeholders with realistic payloads — every mock and SDK downstream consumes them. Also move any x-* vendor extensions deliberately: keep the ones your tooling uses, and check whether 3.1/3.2 now covers them natively (webhooks, content encodings).
Step 9: validate against the real service
A converted document that validates syntactically can still be fiction. Two checks close the gap:
- Diff against traffic. Replay recorded requests/responses (an HAR export works) and validate payloads against the converted schemas. Nullability and missing required fields show up immediately.
- Generate a client and call the API. Regenerate the TypeScript or Java client, run the smoke tests, and compare against the old client. A field typed wrong is obvious the moment code compiles differently.
If the service exists but the spec was neglected, consider scanning the codebase with an AST-based extractor and merging its findings into the converted document — the scan catches endpoints added to the code but never to the 2.0 file, which is nearly always some.
Step 10: wire the new document into the lifecycle
A migration that ends with a file in a wiki will rot again by next quarter. On the day of cutover:
- Put the spec in git with a lint/validation step in CI.
- Add breaking-change detection so future reviews show the diff.
- Render docs, generate mocks, and — if agents touch this API — serve the document as an MCP endpoint.
- Pin the OpenAPI version in the document and in every consumer's toolchain.
The migration is complete when the spec is consumed, not when it validates.
Powerduck opens both 2.0 and 3.x documents, assists the conversion review with explicit gap flags, and then drives mocks, scenario tests, docs, and MCP from the finished 3.2 file; the demo has a sample spec to test the result against.
What to read next: OpenAPI 3.2 in 2026: what changed for SSE and AI agents covers the target version in depth, and detect breaking API changes in CI sets up the post-migration gate.