Your API exists as a Postman collection of six saved requests, five of them in two folders. It
holds eight example responses, a {{baseUrl}} variable, an API key on the collection, and a bearer
token on one folder. A partner asks for an OpenAPI document. The converter you run writes http://{{baseurl}}
as the server, two http security schemes called noauth and apikey, and a bare type: object
on every response.
What you get
You will end up with a converter baseline, a prompt that gives a coding agent the collection and its gaps, and two checks on what the agent writes. This is for you if the collection is your only description.
Short answer
Run postman-to-openapi on the v2.1 export for a baseline that keeps every request and example
and loses the variable, the auth, and every schema. Then give a coding agent the collection, the
baseline, and a rule list. The rules resolve {{baseUrl}} into servers, turn collection and
folder auth into securitySchemes, build schemas from the saved examples, and mark a field
required only when every example carries it. Check the result by round-tripping it through
openapi-to-postman.
You will need
Node 22 or later and a Postman collection exported in the v2.1 format, with saved example
responses on its requests. Verified 2026-10-21 against Node 22.22.0, postman-to-openapi 3.0.1,
openapi-to-postmanv2 6.4.0, and yaml 2.9.1. Export as v2.1 even though the current Postman app
has moved its own collection format
to 3.0, because the converter reads only 2.1 and 2.0.1
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Apidog import | A team that will keep working in a GUI and export OpenAPI from it, auth included | Another product between you and the file, and an export whose shape the product decides, with its own extension fields | The document must be reviewed and diffed in git by the people who own the API |
| Claude Code | Gaps a converter cannot fill: the variable, folder auth, schemas from examples, prose from names | A draft that can invent a field or mark every one required, so each rule it follows needs a check that it did | The collection saved no example responses, so every schema would be a guess |
openapi-to-postman | The check on the result: a document that round-trips to the same requests kept them | It rebuilds one saved response per status code, so a lost example, a wrong type, or a missing description passes it | You want the schemas validated, which is a job for a linter |
postman-to-openapi | The mechanical baseline in one command, with folders as tags, requests as operations, and examples kept | An archived project writing OpenAPI 3.0, with a bare type: object on every response, both auth types mapped to schemes the specification lacks, and the server variable mangled | The collection was generated from a specification you still have |
The converter is deterministic and fast. What it loses is most of what made the collection useful: the variable that names the server, the token the Payments folder carried, and the shape of every response. The agent fills those gaps from the same file and will, left to itself, treat one example as a schema. Apidog does the same job behind a GUI and hands you its export format. The round trip is how you find out whether any of the three kept the requests.
Run the converter for the mechanical baseline
postman-to-openapi reads the export, turns folders into tags, requests into operations, Postman
path variables into path parameters, and saved responses into examples. convert.mjs runs it and
then reports, from the YAML it wrote, what arrived and what did not.
openapi: 3.0.0
info:
title: Billing API
description: Invoices and payments for the billing service.
version: 1.0.0
servers:
- url: http://{{baseurl}}
components:
securitySchemes:
noauthAuth:
type: http
scheme: noauth
apikeyAuth:
type: http
scheme: apikey
security:
- apikeyAuth: []
node convert.mjs
baseline.yaml from collection.json, postman-to-openapi 3.0.1
openapi version 3.0.0
servers http://{{baseurl}}
securitySchemes noauthAuth: type http, scheme noauth; apikeyAuth: type http, scheme apikey
tags Invoices, Payments
operations 6, from 6 requests
operationIds 0 of 6
path parameters 2 declared, 0 paths still carry a :variable
response examples 7 of 8 media types, from 8 saved examples
response schemas 0 of 8 media types have properties, items, or a $ref
problem+json 0 of 2 saved problem+json responses keep that media type
request body schemas 0 of 2 have properties
folder auth Payments bearer, document security [{"apikeyAuth":[]}]
Six requests became six operations and eight saved responses became examples, the part worth
keeping. The rest is the work order. The server is the variable with its name in
lower case, which Postman itself could no longer resolve. The API key and the health check’s
noauth both became http schemes with made-up scheme values, and the Payments folder’s bearer
token is gone. Every schema is type: object with no properties, two problem responses lost their
application/problem+json media type, and no operation has an id or a description.
The project that wrote this is archived, and its
engines field stops at Node 19. Pin 3.0.1 and npm installs it on Node 22 with a warning. Leave
the version off and npm’s version picker prefers a
release whose engines requirement is satisfied, which here meant 1.7.3, the last release with no
engines field, and one that ignores saved examples.2
Hand the agent the collection, the baseline, and the rules
The prompt names both files, says what the converter lost, and turns each gap into a rule with a check at the end. Rule 5 is the one that matters most and the one an agent will not apply on its own.
Convert collection.json, a Postman collection v2.1 export, into an OpenAPI 3.1 document at
openapi.yaml. baseline.yaml is what postman-to-openapi produced from the same file; start from
it and fix what it lost.
Rules:
1. Resolve `{{baseUrl}}` from the collection's `variable` list into `servers`. Leave no `{{`
anywhere in the result.
2. Turn the collection-level auth into `components.securitySchemes` and a document-level
`security` requirement. Turn the folder-level auth on Payments into a second scheme applied
to that folder's operations. A request with `noauth` gets `security: []`.
3. Keep the path parameters the baseline declared. Drop the `Content-Type` header parameters
it invented, and give each operation a snake_case verb-noun operationId.
4. Every saved example response becomes a response with that status code, an `examples` entry
named after the saved response, and its body kept verbatim. Do not invent, shorten, or
tidy a value.
5. Derive each response schema from the saved examples and put it under
`components.schemas`. A field is `required` only when it appears in every saved example of
that shape, and only when there are at least two such examples. With one example, leave
every field optional.
6. Write `summary` from the request name and `description` from the request and folder
descriptions. Keep the media type each saved response was served with, so a problem body
stays `application/problem+json`.
7. After each batch of edits, run `node required.mjs openapi.yaml` and
`node roundtrip.mjs openapi.yaml`, and stop when both pass. Do not touch collection.json.
No model is called by anything on this page. openapi.yaml and openapi.greedy.yaml were
written for it as recorded stand-ins for the agent’s second and first drafts, so every check
below runs offline and prints the same thing every time. The second draft resolves the auth the
way rule 2 asks:
components:
securitySchemes:
apiKey:
type: apiKey
in: header
name: X-API-Key
description: The collection-level auth, with the apiKey variable as its value.
paymentsToken:
type: http
scheme: bearer
description: The folder-level auth on Payments, with the paymentsToken variable as its value.
The two payment operations carry security: [{ paymentsToken: [] }], the health check carries
security: [], and everything else inherits the document-level apiKey requirement, which is
what the collection’s inherit-from-parent rule meant all along.
Mark a field required only when two examples agree
A saved example response is one sample, and an agent asked for a schema reads a sample as a
contract: every field present becomes required. The list response here has three invoices across
two saved examples, and one of them carries memo. required.mjs walks each example alongside
its schema, counts the instances of each named schema that carry each field, and compares that
with the required lists.
const refName = (schema) => schema?.$ref?.replace('#/components/schemas/', '')
/** Walk an example value alongside its schema and record every instance of a named schema. */
export function collect(doc, schema, value, instances) {
if (schema == null || value == null) return
const name = refName(schema)
if (name) {
instances.set(name, [...(instances.get(name) ?? []), value])
return collect(doc, doc.components.schemas[name], value, instances)
}
// OpenAPI 3.1 writes a nullable array as type: [array, 'null'], so type may be a list.
if ([].concat(schema.type).includes('array') && Array.isArray(value)) {
for (const item of value) collect(doc, schema.items, item, instances)
} else if (schema.properties && typeof value === 'object') {
for (const [key, sub] of Object.entries(schema.properties)) collect(doc, sub, value[key], instances)
}
}
node required.mjs openapi.yaml
openapi.yaml: required lists against the saved examples
InvoicePage 2 instances
in all: data, next_cursor
required: data, next_cursor ok
Invoice 5 instances
in all: id, status, customer_id, total, currency, due_at
in some: memo (3 of 5), paid_at (1 of 5)
required: id, status, customer_id, total, currency, due_at ok
Problem 2 instances
in all: type, title, status
in some: errors (1 of 2), detail (1 of 2)
required: type, title, status ok
Payment 2 instances
in all: id, invoice_id, amount, currency, method, recorded_at
in some: reference (1 of 2)
required: id, invoice_id, amount, currency, method, recorded_at ok
PaymentPage 1 instance, so nothing may be required
in all: data, next_cursor
required: (none) ok
Invoice appears five times across the saved responses: three list items, one created invoice,
and one fetched one. Six fields are in all five and two are not, so memo and paid_at are
optional and the rest are required. PaymentPage appears once, so its required list is empty.
One example is a fact about one response, not a rule about all of them.
Round-trip the result back to a collection
openapi-to-postman converts the other
way, and its folderStrategy=Tags option rebuilds folders from tags. roundtrip.mjs converts the
document and compares the result with the original collection: the request count, and the method
and path of each request with its variables blanked. The converter writes one saved response per
status code, so the script finds each saved body among the document’s examples instead.
/** METHOD and path of one request, with the base URL variable dropped and path variables blanked. */
export function signature(item) {
const url = item.request.url
const parts = typeof url === 'string' ? url.replace(/^\{\{baseUrl\}\}/, '').split('?')[0].split('/').filter(Boolean) : url.path
const path = '/' + parts.map((p) => (p.startsWith(':') || p.startsWith('{') ? '{}' : p)).join('/')
return `${item.request.method} ${path}`
}
node roundtrip.mjs openapi.yaml
collection.json -> openapi.yaml -> openapi-to-postman 6.4.0
requests 6 in the collection, 6 in the round trip
GET /health both
GET /invoices both
GET /invoices/{} both
GET /payments both
POST /invoices both
POST /invoices/{}/payments both
saved examples 8 in the collection, 8 kept verbatim in openapi.yaml
variables baseUrl=https://api.example.com/v1
Six requests out, six back, under the same methods and paths, all eight saved bodies among the
examples, and a baseUrl variable that resolves. The script exits 1 on a request present on one
side only or a saved body the document lost, so it can gate a pull request that edits the document.
Check it worked
Six tests pin the converter’s output, the agent document, the two checks, and the round trip.
node --test convert.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1960.718592
The third test reads every examples entry in openapi.yaml and compares it, parsed, with the
body of the saved response it is named after. It then counts them, so an example with no saved
response behind it fails the test. The first test regenerates the baseline from the collection and
asserts it equals the committed copy, so a converter upgrade that changes its output fails here
rather than on the page.
When it goes wrong
Every field is required. openapi.greedy.yaml stands in for the first draft an agent writes
unprompted, with each schema’s required list equal to its property list.
node required.mjs openapi.greedy.yaml
openapi.greedy.yaml: required lists against the saved examples
InvoicePage 2 instances
in all: data, next_cursor
required: data, next_cursor ok
Invoice 5 instances
in all: id, status, customer_id, total, currency, due_at
in some: memo (3 of 5), paid_at (1 of 5)
required: id, status, customer_id, total, currency, due_at, memo, paid_at wrong: memo, paid_at not in every example
Problem 2 instances
in all: type, title, status
in some: errors (1 of 2), detail (1 of 2)
required: type, title, status, detail, errors wrong: detail, errors not in every example
Payment 2 instances
in all: id, invoice_id, amount, currency, method, recorded_at
in some: reference (1 of 2)
required: id, invoice_id, amount, currency, method, reference, recorded_at wrong: reference not in every example
PaymentPage 1 instance, so nothing may be required
in all: data, next_cursor
required: data, next_cursor wrong: one example is not agreement
Four of five schemas are wrong, and a client that validates responses against this draft would
reject the first page of invoices, whose second invoice has no memo. Hand the agent this output,
not a general complaint: it names the seven fields to take out of four required lists.
The round trip passes a document you know is bad. Run it on the baseline and it reports six
requests, all eight saved bodies, and baseUrl=http://{{baseurl}} without complaint, because it
compares requests and example bodies and reads no schema. Pair it with required.mjs and a
linter; the page on setting up a Spectral lint loop for an agent editing OpenAPI
has the ruleset.
A problem response comes back as application/json. The converter reads the saved response’s
Content-Type header into a response header and still files the body under application/json,
so rule 6 exists to put application/problem+json back.3 Check the media types in the result
against the saved headers, which the fifth test does by name.
When not to do this
Do not hand an agent a collection that still carries secrets. An export holds the current value
of every variable and every auth field, and apiKey is empty here only because the sample was
written that way. Blank the values before the file reaches a prompt, or they reach the
document’s examples and your git history.
Do not convert a collection that was generated from a specification you still have. A specification derived from a derived collection carries the losses of both conversions.
Do not let the agent decide required from one example. The greedy draft is what that produces,
and the two-example rule is cheap to state and cheap to check.
Do not publish the converter’s output as the document. It is OpenAPI 3.0 from an archived project, its two security schemes fail the specification, and its schemas say nothing. It earns its place as a baseline that keeps the requests and the examples, and nowhere else.
Do not reach for an agent when a GUI is where the team lives. Apidog
imports the collection with its folders, examples, variables, and auth, and exports OpenAPI 3.1
with the auth in securitySchemes, at the price of its extension fields in the export.
Related how-tos
Last verified
Verified 2026-10-21 against Node 22.22.0, postman-to-openapi 3.0.1, openapi-to-postmanv2
6.4.0, and yaml 2.9.1. Every output block is what the command preceding it printed, run in the
page’s code directory after npm ci, which warns that 3.0.1 declares Node 14 to 19 and installs
it anyway. No model was called: openapi.yaml and openapi.greedy.yaml are committed stand-ins
for a coding agent’s second and first drafts, written by hand to the prompt on this page. Apidog
was not run; what the page says about it is read from its documentation. The 1.7.3 result was
observed on an unpinned install in the same session and is not captured on the page.
Footnotes
-
Postman documents that “the current version of Postman uses schema 3.0.0” for its collections, and that Newman runs a collection exported to the 2.1.0 format and cannot run a 3.0 one. The converter on this page was archived before 3.0 existed and reads 2.1 and 2.0. So the format the app now keeps collections in is one that neither its own runner nor the converter reads, and 2.1 remains the export everyone asks for. ↩︎ Back to text
-
The project’s README says it “is no longer used or maintained” and invites forks, while the
package.jsonbeside it declares"node": ">=14 <20". npm reads that field when it resolves an unpinned version and prefers a release whose engines admit the running Node, which is how a 2026 install of an unpinned package produced 1.7.3. The newest release is the one you have to ask for by number. ↩︎ Back to text -
application/problem+jsonis the media type RFC 9457 registers for its problem details object, withtype,title,status,detail, andinstanceas the members. The saved 404 carries four of the five, and the 422 three plus anerrorsarray, which the RFC permits as an extension member. The converter filed both underapplication/jsonregardless. ↩︎ Back to text