To convert Swagger 2.0 to OpenAPI 3, run npx swagger2openapi swagger.yaml -o openapi.yaml. The command reads the 2.0 file and writes an OpenAPI 3.0.0 document: host, basePath and schemes become a servers entry, definitions move to components/schemas, body and formData parameters become a requestBody, and every $ref is rewritten. That takes seconds. The output is valid, but it is a literal translation, so it keeps 2.0 workarounds you no longer need and copies schemas into places where they may be wrong. Plan a short review pass after the command: fix the security scheme, trim media types, check uploads and array parameters, then validate. This guide covers the conversion tools, a worked example with the converter's real output, and the checklist for that review.
Run swagger2openapi from the command line
swagger2openapi is an MIT-licensed npm package from the Mermade oas-kit project. The commands below were run against version 7.0.8. You can run it once with npx, or install it as a dev dependency so the version is pinned for your team.
# One-off conversion (output format follows the file extension)
npx swagger2openapi swagger.yaml -o openapi.yaml
# Convert a hosted spec and print YAML to stdout
npx swagger2openapi https://petstore.swagger.io/v2/swagger.json --yaml
# Pin it in the project
npm install --save-dev swagger2openapi
npx swagger2openapi swagger.json -o openapi.json --patch| Flag | What it does | When to use it |
|---|---|---|
| -o, --outfile | Writes the result to a file instead of stdout | Almost always |
| -y, --yaml | Writes YAML instead of JSON | When printing to stdout and you want YAML |
| -p, --patch | Fixes small errors in the source while converting | Older 2.0 files that never passed strict validation |
| -w, --warnOnly | Adds x-s2o-warning extensions instead of failing on problems it cannot patch | To see every problem in one run instead of stopping at the first |
| -r, --resolve | Resolves external $ref files | Specs split across several files |
| -b, --rbname | Stores each body parameter's name in an extension you name | When generated client code used the 2.0 body parameter name |
| -t, --targetVersion | Overrides the default target of 3.0.0 | Rarely; see the 3.1 note below |
The package also exposes a Node API, which is handy in a build script. convertObj takes the parsed 2.0 document and an options object, and resolves with the same options object; the converted document is on its openapi property.
const fs = require("node:fs");
const YAML = require("yaml");
const converter = require("swagger2openapi");
const swagger = YAML.parse(fs.readFileSync("swagger.yaml", "utf8"));
converter
.convertObj(swagger, { patch: true, warnOnly: true })
.then((options) => {
fs.writeFileSync("openapi.yaml", YAML.stringify(options.openapi));
});Use a hosted converter instead of npx
SmartBear runs a hosted converter at converter.swagger.io, built on the Java swagger-parser. It has an HTTP API, so you can convert from curl without installing Node. POST the file, or pass a public URL as a query parameter. Set the Accept header to choose JSON or YAML output.
# Upload a local file
curl -X POST https://converter.swagger.io/api/convert \
-H "Content-Type: application/yaml" \
-H "Accept: application/yaml" \
--data-binary @swagger.yaml > openapi.yaml
# Convert a public URL
curl "https://converter.swagger.io/api/convert?url=https://petstore.swagger.io/v2/swagger.json" \
-H "Accept: application/json" > openapi.jsonTwo cautions apply. The upload sends your spec to a third-party server, so do not use it for internal APIs your company has not cleared for that. Its output also differs from swagger2openapi in small ways: it targets openapi 3.0.1, adds an x-original-swagger-version extension, keeps the body parameter name as x-codegen-request-body-name, and writes content: {} on responses that have no body. The classic Swagger Editor at editor.swagger.io has an Edit menu entry, Convert to OpenAPI 3, that uses the same service. For anything you run in CI, prefer swagger2openapi so the conversion happens on your machine with a pinned version.
Where each 2.0 field ends up
| Swagger 2.0 | OpenAPI 3.0 after conversion | Review needed |
|---|---|---|
| host, basePath, schemes | servers: one URL per scheme | Add staging or regional servers; with no host, swagger2openapi writes no servers and converter.swagger.io writes url: / |
| definitions | components/schemas, with $refs rewritten | Rarely |
| parameters with in: body | requestBody with one content entry per consumes type | Body parameter name is lost unless you keep it in an extension |
| parameters with in: formData | requestBody with multipart/form-data or application/x-www-form-urlencoded | Yes, check file fields and per-part encoding |
| consumes, produces | Media type keys under content | Yes, every type gets a copy of the same schema |
| securityDefinitions | components/securitySchemes | Yes, the 2.0 types are copied as they are |
| Response schema | content, one entry per produces type | Same as produces |
| Response examples keyed by media type | examples or example under each media type | Rename the generated keys |
| collectionFormat | style and explode | tsv has no 3.0 equivalent |
| type: file | type: string, format: binary | Check the media type |
| x-nullable: true | nullable: true | Other vendor extensions are left as they are |
Converting a sample Pets API
The sample below has the features that cause most review work: a global produces list with two types, an Authorization header described as an apiKey, a body parameter, a multipart upload, a csv array parameter, a response example and an x-nullable field.
swagger: "2.0"
info:
title: Pets API
version: 1.0.0
host: api.example.com
basePath: /v1
schemes:
- https
consumes:
- application/json
produces:
- application/json
- application/xml
securityDefinitions:
bearerAuth:
type: apiKey
in: header
name: Authorization
security:
- bearerAuth: []
paths:
/pets:
get:
operationId: listPets
parameters:
- name: tags
in: query
type: array
items:
type: string
collectionFormat: csv
responses:
"200":
description: A list of pets
schema:
type: array
items:
$ref: "#/definitions/Pet"
examples:
application/json:
- id: 1
name: Rex
tag: null
post:
operationId: createPet
parameters:
- in: body
name: pet
required: true
schema:
$ref: "#/definitions/Pet"
responses:
"201":
description: Created
schema:
$ref: "#/definitions/Pet"
/pets/{id}/photo:
post:
operationId: uploadPhoto
consumes:
- multipart/form-data
parameters:
- name: id
in: path
required: true
type: integer
- name: file
in: formData
type: file
required: true
- name: caption
in: formData
type: string
responses:
"204":
description: Uploaded
definitions:
Pet:
type: object
required: [id, name]
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
x-nullable: trueopenapi: 3.0.0
info:
title: Pets API
version: 1.0.0
security:
- bearerAuth: []
paths:
/pets:
get:
operationId: listPets
parameters:
- name: tags
in: query
style: form
explode: false
schema:
type: array
items:
type: string
responses:
"200":
description: A list of pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
examples:
response:
value:
- id: 1
name: Rex
tag: null
application/xml:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
post:
operationId: createPet
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
required: true
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
application/xml:
schema:
$ref: "#/components/schemas/Pet"
"/pets/{id}/photo":
post:
operationId: uploadPhoto
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
caption:
type: string
required:
- file
required: true
responses:
"204":
description: Uploaded
servers:
- url: https://api.example.com/v1
components:
securitySchemes:
bearerAuth:
type: apiKey
in: header
name: Authorization
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
nullable: trueThe mechanical work is correct. The csv array became style: form with explode: false, which serializes the same way. The upload became a multipart/form-data requestBody with the file as type: string, format: binary, and the required list moved into the schema. x-nullable became nullable: true. Four things still need a person.
- bearerAuth is still type: apiKey on the Authorization header, the usual 2.0 workaround for bearer tokens. Clients generated from it will not add the Bearer prefix.
- Every response now lists application/xml with the same schema as JSON. If the API returns XML for real, the Pet schema probably needs xml settings; if it never did, the entry is noise.
- The response example became a named example called response under examples. That works, but a single example reads better as example.
- The body parameter name, pet, is gone. Code generators that named the method argument after it will now pick their own name.
openapi: 3.0.3
info:
title: Pets API
version: 1.0.0
servers:
- url: https://api.example.com/v1
security:
- bearerAuth: []
paths:
/pets:
get:
operationId: listPets
parameters:
- name: tags
in: query
style: form
explode: false
schema:
type: array
items:
type: string
responses:
"200":
description: A list of pets
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Pet"
example:
- id: 1
name: Rex
tag: null
post:
operationId: createPet
x-codegen-request-body-name: pet
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearerNine fixes to make after the converter runs
Work through this list on any converted file. Each item comes from a difference between what the 2.0 file said and what you probably meant, which no converter can infer.
Review after converting Swagger 2.0 to OpenAPI 3
- Replace apiKey bearer workarounds with an http schemeAn apiKey named Authorization in the header nearly always means a bearer token. Change it to type: http, scheme: bearer, and add bearerFormat: JWT if that is what you issue. Basic auth converts to type: http, scheme: basic on its own.
- Trim or split media types copied from produces and consumesConverters give every listed type the same schema. Remove types the endpoint does not serve, and give XML or CSV their own schema where the body differs.
- Check vendor extensions beyond x-nullableBoth converters turned x-nullable into nullable: true in our test. Extensions specific to a generator or gateway, such as x-omitempty or x-ms-* keys, pass through untouched. Search for x- and decide what each one should become.
- Fix collectionFormat values with no 3.0 equivalentcsv maps to style: form, explode: false, multi to explode: true, and pipes to pipeDelimited. tsv has no 3.0 style. swagger2openapi stops with an error on it, or with --warnOnly leaves an x-collectionFormat key and an x-s2o-warning for you to resolve.
- Review file uploads and downloadsformData file fields become format: binary properties in a multipart schema. Add an encoding entry if a part needs a specific contentType. A 2.0 response with type: file becomes a binary string under each produces type, so make sure that type is the real one, such as application/pdf.
- Tidy examplesswagger2openapi turns response examples into named examples called response; converter.swagger.io uses example. Rename them or switch to a single example. x-example on a parameter becomes example.
- Add discriminator mappingsA 2.0 discriminator is a property name, and the value had to equal the schema name. Converters write discriminator: { propertyName } and nothing more. If your payloads send values like cat instead of Cat, add a mapping, and consider a oneOf so clients know the allowed subtypes.
- Restore body parameter names if codegen depends on themUse swagger2openapi -b x-codegen-request-body-name, or add the extension by hand, so generated method signatures stay stable.
- Set servers and the version lineAdd every environment to servers, remove a bare url: / if the converter wrote one, and raise openapi: 3.0.0 to 3.0.3 to get the clarified spec text. Remove content: {} from bodyless responses.
Validate the converted file
Validate twice: once to prove the file is valid OpenAPI 3, and once to prove nothing went missing. For the first, swagger2openapi ships an oas-validate command, and Redocly CLI adds lint rules on top of structural checks. Both run without installing anything globally.
# Structural validation, from the same package
npx -p swagger2openapi oas-validate openapi.yaml
# Structural checks plus lint rules
npx @redocly/cli lint openapi.yamloas-validate passed the converted sample. Redocly's recommended ruleset failed it, on missing operation summaries and missing 4xx responses. Those gaps were already in the 2.0 file. Conversion changes the format and leaves design problems exactly where they were, so expect lint failures on a file that never had a linter, and fix or ignore them as a separate task.
For the second check, load the old and new files side by side and compare operation counts, parameters and response codes. Paste each into the Swagger 2.0 validator and the OpenAPI validator; APIForge reads both versions, lists every operation, and lets you send a real request to confirm the server URL and the auth header still work. If the parser complains, common OpenAPI errors has fixes for the usual messages, and how to validate an OpenAPI or Swagger spec shows how to run these checks in CI so later edits stay valid. The glossary entry on OpenAPI covers the terms used here.