news 2026/9/10 22:00:13

Swagger UI 错误转换器(Error Transformers)完全指南:解析、实现与扩展机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Swagger UI 错误转换器(Error Transformers)完全指南:解析、实现与扩展机制

Swagger UI 错误转换器(Error Transformers)完全指南:解析、实现与扩展机制

【免费下载链接】swagger-uiSwagger UI is a collection of HTML, JavaScript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.项目地址: https://gitcode.com/GitHub_Trending/sw/swagger-ui

本指南系统讲解 Swagger UI 错误子系统中的Error Transformers(错误转换器)机制:它如何通过统一接口把机器生成的原始错误消息改写成对终端用户更有用的提示,覆盖输入输出契约、内置转换器实现、删除错误语义、以及如何在钩子(hook)中注册自己的转换器。读完本文,你将掌握 Swagger UI 错误流水线的完整原理,并能独立编写、注册、测试自定义错误转换器。

什么是 Error Transformers

Swagger UI 在解析 OpenAPI/Swagger 规格文件时,底层 JSON Schema 校验器会生成大量面向开发者的原始错误(例如is not of a type(s) string)。这些消息虽然准确,但对普通 API 使用者而言不够友好——用户更希望看到“这个字段应该是什么类型”“问题出在哪一行”这类可读信息。

Error Transformers 正是为此设计的标准化接口层:每个转换器通过transform函数,把一组原始错误转换为结构相同但内容更友好的一组错误。它的设计目标非常克制——不改变错误的整体结构,只优化其中的信息表达,让"生成的错误消息对终端用户更有用"。

在 Swagger UI 的插件体系中,err 插件(位于 src/core/plugins/err/index.js)负责错误状态管理,而转换器是其核心处理环节。该插件的完整代码组织如下:

src/core/plugins/err/ ├── actions.js # 错误相关的 Redux actions ├── index.js # err 插件入口(statePlugins.err) ├── reducers.js # Reducers,内部调用 transformErrors ├── selectors.js # allErrors / lastError 选择器 └── error-transformers/ ├── README.md # 本文档的源头(转换器规范) ├── hook.js # transformErrors 钩子:注册并串联所有转换器 └── transformers/ ├── not-of-type.js # 内置转换器:美化 "is not of a type(s)" 错误 └── parameter-oneof.js # 内置转换器:参数 oneOf 校验错误的定制化(当前禁用)

输入与输出契约:transform 函数的标准接口

输入:Immutable List of Immutable Maps

每个转换器的transform函数接收的第一个参数是一个 Immutable List,其元素是 Immutable Map。也就是说,传入的是 Immutable.js 的List,其中每个条目是一个Map,代表一条错误记录。例如:

List([ Map({ path: "info.version", message: "is not of a type(s) string" }), Map({ path: "info.license", message: "is not of a type(s) object" }) ])

输出:同样形状的 List

transform函数必须返回同样形态的 List——即由结构相近的 Map 组成的列表。转换器可以改写、增删错误条目,但返回的整体容器类型与条目形状应当保持一致,以便流水线继续处理。

错误来源:Redux 错误 actions

这些错误源自向 Redux 状态中添加错误的错误 actions。在 src/core/plugins/err/actions.js 中定义了五类错误 action:

Action 名称Action Creator用途
NEW_THROWN_ERRnewThrownErr(err)新增一条运行时抛出的错误(自动serializeError
NEW_THROWN_ERR_BATCHnewThrownErrBatch(errors)批量新增运行时错误
NEW_SPEC_ERRnewSpecErr(err)新增一条规格(spec)解析错误
NEW_SPEC_ERR_BATCHnewSpecErrBatch(errArray)批量新增规格错误
NEW_AUTH_ERRnewAuthErr(err)新增一条鉴权错误
CLEARclear(filter){type: 'spec'}{source: 'parser'}等条件清除错误
CLEAR_BYclearBy(fn)用谓词函数清除错误

关键点在于:错误在进入 reducer 之前,会先经过转换器。以 src/core/plugins/err/reducers.js 为例,每次向状态写入错误后都会立即调用transformErrors

import transformErrors from "./error-transformers/hook" [NEW_SPEC_ERR]: (state, { payload }) => { let error = fromJS(payload) error = error.set("type", "spec") return state .update("errors", errors => (errors || List()).push(fromJS(error)).sortBy(err => err.get("line")) ) .update("errors", errors => transformErrors(errors)) },

也就是说,transformErrors是 reducer 写入错误后的必经环节,转换发生在“错误进入 Redux state”之前——这正是 README 中所述"Errors are transformed before being passed into the reducer"的准确含义。

必须保留的错误键

README 强调:当转换器处理完一条错误时,必须保证该错误中已有的所有键仍然存在,尤其是以下五个键:

  • line—— 错误所在的行号
  • level—— 错误级别(如errorwarning
  • message—— 错误消息内容
  • source—— 错误来源(如parserstructural
  • type—— 错误类型(如specthrownauth

这些键是错误在 UI 层渲染和排序的基础。在 src/core/components/errors.jsx 中,SpecErrorItem会读取sourcelevelpathlinemessage来展示"来源 + 级别 + 位置"的错误条目;sortedJSErrors也按line排序。因此,一个转换器若意外丢掉这些键,会导致错误面板渲染异常。

另外,reducers.js 中的DEFAULT_ERROR_STRUCTURE定义了缺省值:

let DEFAULT_ERROR_STRUCTURE = { line: 0, level: "error", message: "Unknown error" }

当 action 负载缺少这些字段时,会以默认值补齐,再进入转换流程。

删除一条错误:用 null 覆盖

README 提供了一种优雅的删除语义:如果想彻底删除某条错误,用null覆盖它。转换器返回的数组中,null值会在错误返回前被过滤掉。

这一语义在 src/core/plugins/err/error-transformers/hook.js 中被双重落实:

let transformedErrors = reduce(errorTransformers, (result, transformer) => { try { let newlyTransformedErrors = transformer.transform(result, inputs) return newlyTransformedErrors.filter(err => !!err) // 过滤被删除的错误 } catch(e) { console.error("Transformer error:", e) return result } }, errors) return transformedErrors .filter(err => !!err) // 再次过滤被删除的错误 .map(err => { /* ... */ })

可以看到:

  1. 每个转换器执行后,立即filter(err => !!err)剔除null
  2. 全部转换器串联结束后,再次.filter(err => !!err)兜底过滤;
  3. 单个转换器若抛出异常,会被try/catch捕获并打印Transformer error:日志,同时返回未经该转换器修改的原结果,保证流水线不因单个转换器故障而中断。

这种"以null表示删除"的设计,让转换器在保留 List 形状的同时,可以自由表达"我要移除这条错误"的意图。

注册与串联:transformErrors 钩子(hook.js)

src/core/plugins/err/error-transformers/hook.js 是整个转换机制的装配中心。其核心逻辑用lodash/reduce把所有转换器串联成一个管道:

import reduce from "lodash/reduce" import * as NotOfType from "./transformers/not-of-type" import * as ParameterOneOf from "./transformers/parameter-oneof" const errorTransformers = [ NotOfType, ParameterOneOf ] export default function transformErrors (errors) { let inputs = { jsSpec: {} // 预留给 spec 上下文,见下文说明 } let transformedErrors = reduce(errorTransformers, (result, transformer) => { try { let newlyTransformedErrors = transformer.transform(result, inputs) return newlyTransformedErrors.filter(err => !!err) } catch(e) { console.error("Transformer error:", e) return result } }, errors) return transformedErrors .filter(err => !!err) .map(err => { if(!err.get("line") && err.get("path")) { // TODO: re-resolve line number if we've transformed it away } return err }) }

值得注意的实现细节:

  1. 第二个参数inputstransform函数可以接收第二个参数inputs。当前hook.js传入的是{ jsSpec: {} }。源码注释明确说明这是一个"未实现的遗留物"(unimplemented artifact)——理想情况下jsSpec应指向system.specSelectors.specJS()获取真实规格对象,且为兼容 redux@4,jsSpec应作为参数向下传递而不是在内部调用 store 方法。目前它仅作为占位符,供依赖规格上下文的转换器(如parameter-oneof)使用。

  2. 错误隔离:每个转换器都被try/catch包裹,单个转换器抛错不会影响整个错误列表——错误被console.error("Transformer error:", e)记录,并回退到转换前的result

  3. 行号重解析的 TODO:流水线末尾对"有path但无line"的错误预留了重解析行号的 TODO 分支,但目前未实现。

要添加自定义转换器,只需把新模块导入并追加到errorTransformers数组即可。注意:由于 Swagger UI 仓库是只读的,你需要在自己项目的 Swagger UI 定制构建或插件中完成这一步,而非直接修改仓库源码。

内置转换器一:not-of-type

src/core/plugins/err/error-transformers/transformers/not-of-type.js 是第一个内置转换器,专门处理 JSON Schema 校验器输出的is not of a type(s)错误。

为什么需要它

JSON Schema 校验器把"当前正在校验的对象"称为instance,这类原始消息对用户不友好。例如原始消息:

is not of a type(s) string

用户更希望看到的是自然语言化的:

should be a string

实现原理

export function transform(errors) { return errors .map(err => { let seekStr = "is not of a type(s)" let i = err.get("message").indexOf(seekStr) if(i > -1) { let types = err.get("message").slice(i + seekStr.length).split(",") return err.set("message", err.get("message").slice(0, i) + makeNewMessage(types)) } else { return err } }) } function makeNewMessage(types) { return types.reduce((p, c, i, arr) => { if(i === arr.length - 1 && arr.length > 1) { return p + "or " + c } else if(arr[i+1] && arr.length > 2) { return p + c + ", " } else if(arr[i+1]) { return p + c + " " } else { return p + c } }, "should be a") }

算法要点:

  1. 在每条错误的message中查找子串is not of a type(s)
  2. 若找到,把其后按逗号分割的类型列表提取出来;
  3. 保留消息中seekStr之前的前缀(可能包含路径等上下文信息);
  4. makeNewMessage重新组装一条友好消息,规则为:
    • 单一类型:should be a string
    • 两个类型:should be a string or array
    • 三个及以上类型:should be a string, array, or number(牛津逗号式列举)
  5. 未匹配的消息原样返回,不做修改。

测试用例佐证

单元测试位于 test/unit/core/plugins/err/transformers/not-of-type.js,覆盖三种形态:

// 单个类型 { path: "info.version", message: "is not of a type(s) string" } // → { path: "info.version", message: "should be a string" } // 两个类型 { message: "is not of a type(s) string,array" } // → { message: "should be a string or array" } // 三个类型 { message: "is not of a type(s) string,array,number" } // → { message: "should be a string, array, or number" }

这三个用例分别验证了单数、复数(2 个)与复数(3 个以上)类型列表的格式化逻辑,同时验证了path等键在转换过程中被原样保留。

内置转换器二:parameter-oneof(当前禁用)

src/core/plugins/err/error-transformers/transformers/parameter-oneof.js 是第二个内置转换器,它把参数对象上模糊的 JSON Schema 错误转换为针对具体关键字(incollectionFormat)的可读错误。需要注意的是:该转换器当前处于禁用状态——源码第 5 行注释明确写着"LOOK HERE THIS TRANSFORMER IS CURRENTLY DISABLED",并在transform函数开头直接return errors,后续逻辑成为不可达代码(文件内以/* eslint-disable no-unreachable */抑制告警)。

设计意图(供参考的规范)

其设计目标是把如下原始错误:

is not exactly one from <#/definitions/parameter>,<#/definitions/jsonReference>

拆解为针对具体字段的定制错误。createTailoredParameterError定义了两种可寻址的检查:

  1. in关键字取值检查:合法的in值集合为["path", "query", "header", "body", "formData"](定义于VALID_IN_VALUES)。若parameter.in不在其中,生成错误:

    Wrong value for the "in" keyword. Expected one of: path, query, header, body, formData.
  2. collectionFormat关键字取值检查:合法的取值集合为["csv", "ssv", "tsv", "pipes", "multi"](定义于VALID_COLLECTIONFORMAT_VALUES)。若parameter.collectionFormat不在其中,生成错误:

    Wrong value for the "collectionFormat" keyword. Expected one of: csv, ssv, tsv, pipes, multi.

生成的每条新错误都遵循契约中的五个键:

newErrs.push({ message, path: err.get("path") + ".in", // 精确到出错的子字段 type: "spec", source: "structural", level: "error" })

若两种检查都不命中,则回退返回原错误("fall back to making no changes")。源码中createTailoredParameterError还依赖第二个参数jsSpec,通过get(jsSpec, err.get("path"))从规格对象中取出对应的 parameter 定义——这正是 hook.js 传入{ jsSpec: {} }的目的。

对应的测试位于 test/unit/core/plugins/err/transformers/parameter-oneof.js,但目前以describe.skip挂起,与源码的禁用状态一致。若要重新启用,需要先解决注释中提到的 "flattening problem"(将单条错误展开为多条子错误的扁平化问题)。

从 action 到渲染的完整错误流水线

综合以上源码,一条错误从产生到展示的完整路径是:

错误 action(newSpecErr / newThrownErr / ...) │ ▼ actions.js 组装 payload(补齐 type 等字段) │ ▼ reducers.js 写入 Immutable 状态(.push/.concat + sortBy line) │ ▼ transformErrors(hook.js)串联执行所有转换器 │ ├─ not-of-type:改写 "is not of a type(s)" 消息 │ ├─ parameter-oneof:定制参数错误(当前禁用) │ └─ 自定义转换器(可扩展) │ └─ 逐级 .filter(err => !!err) 剔除 null │ ▼ selectors.js:allErrors / lastError 暴露给 UI │ ▼ errors.jsx:errors-wrapper 渲染(按 line 排序,支持 Jump to line)

其中 src/core/plugins/err/selectors.js 提供了两个基于reselect的选择器:

  • allErrors—— 返回状态中err.get("errors", List())的全部错误;
  • lastError—— 基于allErrors取最后一条。

UI 侧,src/core/components/errors.jsx 中的Errors组件通过errSelectors.allErrors()拉取错误,过滤出thrown类型及level === "error"的错误进行展示,并按line排序;SpecErrorItem负责渲染spec类型错误(显示source + level标题、path/line位置和Jump to line跳转链接),ThrownErrorItem负责渲染运行时错误。

编写自定义 Error Transformer 的实操指南

结合 README 契约与 hook.js 的调用方式,编写一个自定义转换器的完整步骤:

1. 导出标准的transform函数

每个转换器模块只需导出一个transform函数,签名如下:

export function transform(errors, inputs) { // errors: Immutable List of Immutable Maps // inputs: 上下文对象(当前为 { jsSpec: {} }) return errors.map(err => { // 改写 err 的内容,但保留 line / level / message / source / type return err }) }

README 给出的示例——把所有行号加 10:

export function transform(errors) { return errors.map(err => { err.line += 10 return err }) }

2. 遵守错误键契约

修改message等字段时,务必保留linelevelmessagesourcetype五个键。参考not-of-type.js的做法:只err.set("message", ...),其他键不动。

3. 用null表达删除

若某条错误不再有意义,直接return null(或把条目置为null),hook 会自动过滤:

export function transform(errors) { return errors.map(err => { if (shouldRemove(err)) { return null // 该错误将被过滤掉 } return err }) }

4. 注册到errorTransformers数组

hook.jsimport新模块并追加到数组头部或尾部,决定其执行顺序:

import * as MyTransformer from "./transformers/my-transformer" const errorTransformers = [ NotOfType, ParameterOneOf, MyTransformer ]

(在你的定制构建中操作;当前仓库为只读,不应直接改动源码文件。)

5. 用单元测试验证

参照 test/unit/core/plugins/err/transformers/not-of-type.js 的写法,用 Immutable 的List/Map构造输入,断言transform(ori).toJS()的输出:

import { Map, List } from "immutable" import { transform } from "path/to/my-transformer" describe("my transformer", () => { it("should rewrite the message", () => { let ori = List([ Map({ path: "info.version", message: "original message" }) ]) let res = transform(ori).toJS() expect(res).toEqual([{ path: "info.version", message: "friendlier message" }]) }) })

6. 遵循健壮性原则

参考 hook.js 的容错设计:转换器内部建议保持纯函数(不产生副作用),并在异常时优雅回退;生产环境的转换器应避免抛错影响整条错误流水线。

小结

Error Transformers 是 Swagger UI 错误处理子系统中的一层轻量但关键的抽象:

  • 统一接口transform(errors, inputs)输入输出均为 Immutable List of Immutable Maps,契约清晰、易于测试;
  • 管道化执行:hook.js 用reduce串联所有转换器,逐级过滤null并以try/catch隔离故障;
  • 删除语义:用null覆盖即可删除错误,无需破坏 List 形状;
  • 内置范例:not-of-type.js 已投产,将is not of a type(s)消息美化为自然语言;parameter-oneof.js 因扁平化问题暂被禁用,其代码可作为设计参考;
  • 可扩展:新增转换器只需三步——实现transform、注册进errorTransformers数组、补充单元测试。

理解这层机制后,你既可以为自己的 Swagger UI 定制版本编写面向特定场景的友好错误提示,也能在阅读 err 插件其他源码(actions/reducers/selectors)时快速定位转换环节在整体流水线中的位置。

【免费下载链接】swagger-uiSwagger UI is a collection of HTML, JavaScript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.项目地址: https://gitcode.com/GitHub_Trending/sw/swagger-ui

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

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

从零散灵感碎片到完整项目的重建方法论

1. 项目概述 作为一名从业多年的技术博主&#xff0c;我经常遇到一个困扰&#xff1a;当灵感突然来临时&#xff0c;却因为各种原因没能及时记录下完整的项目构思。这种情况在创意工作者和技术开发者中尤为常见——我们可能只来得及写下几个关键词或一个模糊的想法&#xff0c;…

作者头像 李华
网站建设 2026/9/10 21:55:22

PostgreSQL阻塞查询检测与优化实战

1. 为什么需要关注PostgreSQL阻塞查询 在数据库运维过程中&#xff0c;阻塞查询就像交通堵塞中的头车——它不仅自己无法前进&#xff0c;还会导致后方所有依赖它的查询陷入等待状态。我曾在生产环境遇到过一起典型的阻塞案例&#xff1a;一个简单的报表查询阻塞了整个业务系统…

作者头像 李华
网站建设 2026/9/10 21:54:57

Folo 移动端如何用 Expo 在 macOS 上搭建开发环境并跑通 iOS 模拟器

Folo 移动端如何用 Expo 在 macOS 上搭建开发环境并跑通 iOS 模拟器 【免费下载链接】follow &#x1f9e1; Folo is the AI RSS Reader 项目地址: https://gitcode.com/GitHub_Trending/fol/follow Folo 的移动端是一个基于 Expo 的 React Native 应用&#xff0c;代码…

作者头像 李华