news 2026/9/15 10:59:02

从 Postman 集合到 OpenAPI 文档:@scalar/postman-to-openapi 转换引擎的演进与实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 Postman 集合到 OpenAPI 文档:@scalar/postman-to-openapi 转换引擎的演进与实战指南

从 Postman 集合到 OpenAPI 文档:@scalar/postman-to-openapi 转换引擎的演进与实战指南

【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar

@scalar/postman-to-openapi是 Scalar 开源 API 平台中负责把 Postman Collection 转换为 OpenAPI 3.1 文档的核心包,服务于"用 OpenAPI 统一管理 API 描述、摆脱对单一供应商锁定"的导入链路。本文以该包的 CHANGELOG 为骨架,结合 源码 与测试,系统梳理convert/isPostmanCollection的完整用法、全部配置项以及 0.1.0 → 0.7.19 之间的关键能力演进,帮助你理解转换器内部做了什么、为什么这样做,以及如何在项目里正确使用它完成 Postman → OpenAPI 的迁移。

包定位与快速上手

在 Scalar 的包体系中,postman-to-openapi位于packages/postman-to-openapi/,其定位在 README 中写得很直白:把 Postman 集合转换成开放标准 OpenAPI,让使用者从 Postman 的私有格式中解放出来("Free the postman!")。它继承并现代化改造了社区同名项目postman-to-openapi,包名保持了一致。

安装(需要 Node.js >= 22,见 package.json 的engines字段):

npm install @scalar/postman-to-openapi

基本用法:

import { convert } from '@scalar/postman-to-openapi' const result = convert(myPostmanCollection) // 或直接传入集合的 JSON 字符串 console.log(result)

需要说明的是,convert本身是同步函数(类型签名为(postmanCollection, options?) => OpenAPIV3_1.Document,见 convert.ts),README 示例里的await只是无害的演示写法。

包导出的公开 API 集中在 index.ts:

  • convert/ConvertOptions/TagNamingStrategy:核心转换函数与配置;
  • isPostmanCollection:集合识别;
  • extractPathFromUrl/normalizePath:URL 与路径的工具函数,其中normalizePath会把:id风格的路径参数统一改写为{id}(见 urls.ts)。

转换管线的整体流程

convert的执行流程(对应 convert.ts)可以概括为七个阶段,理解这条管线有助于后续章节的内容定位:

  1. 输入解析与校验:字符串输入先JSON.parse,解析失败抛出PostmanCollectionParseError;随后校验info.nameinfo.schemaitem数组等必要字段,缺失即报错(convert.ts)。
  2. 文档骨架初始化:从info.name取标题(缺省'API'),从集合变量version取版本(缺省'1.0.0'),并处理 license、contact、logo(x-logo)、externalDocs 等元数据。
  3. 认证处理:集合级auth与请求级auth都会被转换为 OpenAPI 的securitySchemessecurity
  4. 逐条转换请求:遍历item树,把每个请求转换为 OpenAPI operation——提取 URL 中的路径与服务器、路径参数、请求头、请求体、响应与脚本。
  5. 路径合并与参数统一:将各请求产出的 path item 合入paths,并把"结构等价但参数名不同"的路径合并为一条规范路径。
  6. 服务器分层放置:基于服务器使用频率,决定servers放在文档级、路径级还是操作级。
  7. 清理与剪枝:删除内部簿记扩展字段与空对象,输出整洁的 OpenAPI 3.1.0 文档。

convert 的完整配置项

ConvertOptions是控制转换行为的唯一入口,全部字段及语义如下(见 convert.ts 的类型注释):

配置项类型默认值作用
mergeOperationbooleanfalse是否合并同路径同方法的多个 Postman 请求为一个 OpenAPI operation。false时后出现的请求会覆盖先前请求;true时进入深度合并(示例、参数、脚本按来源保留)
requestIndexPathsreadonly number[][]未设置(全量转换)只转换指定索引路径下的请求。索引从collection.item逐层下钻,最后一项可指向请求或文件夹(文件夹包含全部后代请求);越界或经过非文件夹的路径会被跳过
tagNamingStrategy'leaf' \| 'chain''leaf'OpenAPI tag 命名策略:leaf只用最末层文件夹名(重名时回退为父 / 子);chain保留>连接的全链路
documentOpenAPIV3_1.Document无(新建文档)传入既有 OpenAPI 文档进行增量合并:Postman 路径并入,tag 按名称取并集,securitySchemes 与 servers 合并,既有info与旧路径保留
keepHeadersreadonly string[][]即使命中内置的传输/内容协商/认证头黑名单(如AcceptContent-TypeAuthorizationHost等),仍将这些请求头保留为parameters[in=header]。仅当 API 刻意以非常规方式使用这些头名时才需要

按索引路径筛选请求是批量导入场景的利器。以下示例只转换collection.item[0].item[0].item[0]这一个请求(对应测试 convert.test.ts):

const nestedOnly = convert(collection, { requestIndexPaths: [[0, 0, 0]] }) // 输出 paths 仅含该请求对应路径,且保留该分支的文件夹 tag 上下文

合并进既有文档的用法(同一测试文件中的merges into an existing OpenAPI document用例):

const result = convert(collection, { document: baseOpenApiDocument, // 既有 openapi: '3.1.0' 文档 }) // result.info.title 保持为既有文档的 'Existing',/health 等旧路径被保留

标签命名策略:leaf 默认值与 chain 回退

Postman 的文件夹天然形成层级,OpenAPI 的tags却是扁平的,如何把文件夹层级映射为 tag 名是转换器最早需要回答的问题。0.7.0 之前默认用>拼接完整文件夹链(chain 风格);0.7.0 起改为leaf 优先策略(PR #8900):

  • leaf(默认):tag 名只取最末层文件夹名,并自动附带"Part of 父链"这样的上下文描述(如Parent -> Child层级会生成{ name: 'Child', description: 'Part of Parent' });同名叶子重名时先回退为父 / 子,仍重名则回退为完整链(convert.ts)。
  • chain:保留旧行为,tag 名为父 > 子 > 孙的完整链条,用于兼容既有消费方。
  • 文件夹描述为空字符串时,上下文描述会接管,避免生成无描述的 tag(见测试 convert.test.ts)。
  • 文件夹名若本身是 URL 模板(如/languages/{languageCode}),normalizeLeafTagSegment会将其提炼为可读的叶子标识(如languageCode),避免 tag 名里出现{/}和长路径(convert.ts)。
const result = convert(collection, { tagNamingStrategy: 'leaf' }) // 默认 const legacy = convert(collection, { tagNamingStrategy: 'chain' }) // 兼容旧版行为

服务器 URL 与集合变量解析

Postman 集合几乎总是用{{baseUrl}}{{host}}这类模板变量书写 URL。0.7.0 引入的服务器变量解析(PR #8899)是这条链路上最重要的改进,核心实现在 urls.ts:

  1. createCollectionVariableLookupcollection.variable构建为查找表,跳过disabled与无值变量(urls.ts)。
  2. extractServerObjectFromUrl解析 URL 中的{{...}}模板:
    • 变量值可查且为完整 URL(如https://api.example.com)时直接替换;
    • 变量值可查且为普通字符串(如api.example.com)时拼进服务器 URL;
    • 变量不可解析或值本身又含模板语法(递归变量)时,保留{变量名}占位,并在 OpenAPIservers[].variables中生成默认值为example.com、描述为 "Declared in Postman collection variables." 的变量定义,保证输出文档依然合法且可读(urls.ts)。
// Postman 集合 { "variable": [{ "key": "baseUrl", "value": "https://api.example.com" }], "item": [{ "name": "getUsers", "request": "{{baseUrl}}/users" }] }
// 转换结果(server 部分) { servers: [{ url: 'https://api.example.com' }] }

服务器最终放哪一层由analyzeServerDistribution决定(servers.ts):覆盖全部路径 → 文档级;覆盖多条路径 → 文档级;同路径内多个操作 → 路径级;仅单个操作 → 操作级。这让输出在"少重复"和"表达准确"之间取得平衡。

无响应体状态码与响应描述

0.7.0 默认对无响应体状态码(1xx、204、205、304)省略content字段(PR #8895)。实现在 responses.ts 的hasNoResponseBodyStatusCode:如果 Postman 保存的响应恰好带 body,会打印警告但仍保留 content,避免数据丢失。

响应描述采用状态码感知的默认值(0.6.3,PR #8897):内置DEFAULT_RESPONSE_DESCRIPTIONS映射(200 → "OK"、201 → "Created"、404 → "Not found"、500 → "Internal server error" 等,responses.ts)。当 Postman 响应命名为200 - 用户列表这类状态码 - 描述格式时,extractDescriptionFromName会解析出命名示例并用作响应描述(responses.ts)。

媒体类型的选择优先级为:保存响应自身的Content-Type→ 请求Accept头(pickAcceptMediaType优先application/json)→application/json兜底。响应体示例还会通过inferSchemaFromExample反推 schema,让生成的 OpenAPI 带上有实际值的示例与结构。

另一个值得一提的细节是从测试脚本提取状态码extractStatusCodesFromTests会解析pm.response.to.have.status(201)pm.expect(pm.response.code).to.eql(202)pm.expect(pm.response.status).to.equal('201')三种常见断言模式,把测试中期望的状态码补充到响应定义里(status-codes.ts)。

操作合并与请求变体保留

Postman 中同一个路径+方法常常存在多个请求变体(例如"创建用户 - 成功"、"创建用户 - 参数错误"),而 OpenAPI 的paths不允许重复的路径+方法键。0.6.0 引入mergeOperation支持重复操作(PR #8511),0.6.3 又进一步打磨了合并行为(PR #8902):

  • 请求级示例保留:每个变体的参数示例、请求体示例按来源名保留,重名示例自动生成唯一名(generateUniqueValue#后缀去重);
  • 状态码响应合并:从请求名推导出的状态码响应取并集,而不是后写覆盖;
  • 脚本拼接:pre-request / post-response 脚本按来源名分区存储,最终渲染为以// --- 来源名 ---分隔的拼接文本(merge-operation.ts)。

合并的底层实现在mergeOperations:参数按name/in键去重并浅合并examplesrequestBody.content按媒体类型合并 schema 与示例;responses取并集;summary 取更短者,description 以空行拼接去重后的内容(merge-operation.ts)。

// 让同路径同方法的多个 Postman 请求合并为一个 operation,并保留各自的示例与脚本 const result = convert(collection, { mergeOperation: true })

路径参数统一:结构等价路径合并

Postman 集合里经常出现/users/{id}/users/:userId/users/{{uid}}这样仅参数名不同的"结构等价"路径——它们在 OpenAPI 里会变成两条不同的 key。0.6.3 的unifyEquivalentPathParameters(PR #8898)专门解决这个问题(convert.ts):

  1. getPathStructuralSignature把参数段统一替换为{*},得到结构签名(urls.ts);
  2. 签名相同的路径聚为一组,参数名取出现次数最多者作为规范名(chooseMostCommonName);
  3. 若文件夹名本身是路径模板(如GET /applications文件夹对应/applications/{id}),优先用文件夹模板里的参数名作为规范名;
  4. 其余路径与操作中的 path 参数统一改名,然后mergePathItem合并进规范路径。

该特性保证同一资源的不同写法最终落到唯一路径 key 上,参数定义与路径模板保持一致,避免"半条路径"的碎片化。

脚本导入:pre-request 与 post-response

Postman 的事件脚本(预请求脚本与测试脚本)是很多工作流的关键。0.2.0 起支持导入 post-response 脚本(PR 018e8b2),转换结果写入 OpenAPI 扩展字段:

  • 预请求脚本(listen: 'prerequest')→x-pre-request(pre-request-scripts.ts);
  • 测试脚本(listen: 'test')→x-post-response(post-response-scripts.ts)。

内部还会以x-postman-example-namex-postman-pre-request-scriptsx-postman-post-response-scriptsx-postman-folder-segmentsx-扩展携带来源信息以支撑合并,但在最终输出前cleanupOperations会把这些内部簿记字段全部剥离,确保最终 OpenAPI 文档干净(convert.ts)。

Postman 集合识别与输入健壮性

isPostmanCollection用于在转换前判断输入是否为 Postman 集合(0.6.3 起从本包共享给其他消费方,PR #8903)。识别规则(is-postman-collection.ts)值得注意:导出的集合并不保证包含info._postman_id,因此只要满足以下条件即判定为 Postman 集合:

  • schema URL 的 host 为schema.getpostman.com
  • 且(存在info._postman_id或存在item数组)。
import { convert, isPostmanCollection } from '@scalar/postman-to-openapi' if (isPostmanCollection(input)) { const openApiDocument = convert(input) console.log(openApiDocument) }

输入健壮性还包括:非 JSON 字符串会抛出带PostmanCollectionParseError名称的错误("Invalid Postman collection JSON: ..."),缺info/item/info.schema等关键结构会立即报错而非静默产出残缺文档(convert.ts);0.6.3 还修复了非法 JSON body 的处理(PR #8887)。0.2.0 则把/raw/等特殊路径做了专门处理,并移除了"默认 tag"与凭空捏造的响应——转换器只输出有依据的内容。

工程化与回归保障

0.7.0 起,转换测试不再依赖云端下载,而是把 Postman 转换夹具直接纳入仓库(PR #8901):fixtures/input/ 下存放了 26 个覆盖典型场景的集合,包括AuthBasicAuthBearerAuthMultipleFoldersFormDataFormUrlencodedGetMethodsHeadersMultipleServersNestedServersOperationIdsParseStatusCodePathParamsRawBodyResponsesSimplePostUrlWithPortXLogo等;测试对每个夹具执行convert并与snapshots/convert.test.ts.snap 中的快照比对(convert.test.ts)。夹具输出以 2 空格缩进写入,保证数据未变化时重新生成不会产生噪音 diff。

此外还有覆盖各内部模块的单元测试(如 auth.test.ts、urls 相关测试 等),以及 convert.test.ts 中针对合并、tag 策略、索引筛选、错误输入等行为的用例。

输出阶段还有一道pruneDocument兜底:递归删除所有undefined值,并清掉空tags、空security、空components、空externalDocs,保证文档可直接序列化、可被任何 OpenAPI 工具链消费(prune-document.ts)。

版本演进一览

从 CHANGELOG 可以清晰看到这个包从"hello world"到成熟转换器的演进轨迹:

版本里程碑关键变化
0.1.0诞生"hello world :)",依赖@scalar/oas-utils起步
0.1.8结构修复Postman 示例不再被误判为文件夹
0.2.0脚本与净化导入 post-response 脚本;去掉默认 tag 与捏造响应;/raw/路径专门处理
0.3.0环境基线要求 Node 20+
0.4.0重大重构"major refactor, improves everything, keeps all the data now"
0.5.0环境升级要求 Node 22+(LTS),随@scalar/openapi-types0.6.0 同步升级
0.6.0重复操作mergeOperation支持同路径同方法的多请求
0.6.3合并深化请求变体示例/响应/脚本保留;结构等价路径参数统一;集合识别共享;状态码感知的响应描述
0.7.0转换质量keepHeaders保留头策略;服务器 URL 变量解析与 server variables 生成;leaf tag 默认策略 + chain 回退;无响应体状态码省略 content;夹具入库
0.7.12 / 0.7.17 / 0.7.19发布治理README 生成器元数据修复(scalarReadme);npm trusted publishing 重发,无功能变化

其中 0.7.12 的修复颇具工程启示:npm 会把package.json中的readme字段当作 README 文本本身,导致部分包被发布成字面量[object Object],因此重命名为scalarReadme并重发(PR #9710)。

结语:在 Scalar 生态中的位置

@scalar/postman-to-openapi是 Scalar 导入链路的地基:无论 API Client 中导入 Postman 集合,还是文档平台中把既有 Postman 工作流迁移到 OpenAPI,最终都落在这套转换语义上。0.7.x 系列之后,它已经具备变量解析、变体合并、路径统一、脚本保留、标签策略、增量合并等完整的工业级能力,而仓库内的 26 个夹具与快照测试为后续演进提供了坚实的回归保障。需要进一步深入时,可以从 convert.ts 的ConvertOptions入手,逐层阅读 helpers 下的实现与测试,即可完整掌握每一次转换决策背后的原理。

【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 10:52:45

如何异步导出excel

当用户点击导出按钮时,我们不希望用户等待数据导出的整个过程,因为这可能会很长时间,导致页面超时或者用户体验下降。所以,我们采用异步处理的方式来完成这个任务。这就像是你去餐厅点了一份需要很长时间烹饪的食物,服…

作者头像 李华
网站建设 2026/9/15 10:52:06

C语言实现快速排序算法

1. 什么是快速排序算法快速排序的核心思想是通过分治法(Divide and Conquer)来实现排序。算法的基本步骤是:1. 选择一个基准值(通常是数组中的某个元素),将数组分成两部分,使得左边的部分所有元素都小于基准…

作者头像 李华
网站建设 2026/9/15 10:49:36

Loop:免费开源的 Mac 窗口管理工具,5 分钟就能上手

Loop:免费开源的 Mac 窗口管理工具,5 分钟就能上手 【免费下载链接】Loop Window management made elegant. 项目地址: https://gitcode.com/GitHub_Trending/lo/Loop 第三次把浏览器窗口拖歪、又得手动对齐之后,你终于受够了。Loop 是…

作者头像 李华
网站建设 2026/9/15 10:48:09

Flutter与OpenHarmony结合开发PUBG游戏辅助工具实战

1. 项目背景与核心价值作为一名同时涉足移动开发与游戏领域的全栈工程师,最近我在探索如何将Flutter框架与OpenHarmony操作系统结合,开发一款真正实用的游戏辅助工具。选择PUBG这款现象级战术竞技游戏作为切入点,是因为其复杂的战场决策场景特…

作者头像 李华