基于 Ajv 的通用 JSON Schema 校验引擎:@scalar/json-schema-validator 使用指南与源码解析
【免费下载链接】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 开源仓库中的@scalar/json-schema-validator包展开。它是 Scalar 全家桶中共用的底层校验引擎,负责"用 JSON Schema 校验文档并输出简短、友好的人类可读错误信息",同时也是@scalar/openapi-validator的校验内核。读完本文,你将掌握它的安装方式、validate/createValidator两大 API、方言自动识别、自定义 format 注册与throwOnError异常模式,并能从源码层面理解其编译缓存、错误美化(prettify)与去重流水线的工作原理。
包定位:与 OpenAPI 无关的共享校验内核
@scalar/json-schema-validator在 package.json 中将自己描述为 "Validate documents against a JSON Schema with Ajv and human-friendly errors"。它底层依赖 Ajv 完成实际的 Schema 编译与文档校验,自身则额外提供了两层能力:
- 输入宽容:接受对象,也接受 JSON / YAML 字符串(内部使用
yaml包解析); - 错误友好:把 Ajv 输出的原始错误转换成短小、可读、去重后的人类友好信息。
关键设计原则是:它不感知 OpenAPI 或 AsyncAPI,只处理纯粹的 JSON Schema。因此任何需要"按 Schema 校验 JSON 文档"的场景都可以直接复用它,而 Scalar 生态中的@scalar/openapi-validator正是构建在这一内核之上。从 index.ts 的导出可以看到,这个包对外暴露了四组能力:
validate/createValidator:面向任意 JSON Schema 的通用校验 API(来自 validate.ts);createSpecificationValidator/SpecificationValidatorConfig/SpecificationValidatorOptions:面向 OpenAPI、AsyncAPI 等"规范文档"的高层封装器(来自 create-specification-validator.ts);prettifyAjvErrors/AjvError/PrettyError:错误美化工具(来自 prettify-ajv-errors.ts);deduplicateErrors:错误去重工具(来自 deduplicate-errors.ts)。
安装
包以 npm 包@scalar/json-schema-validator形式发布,需要 Node.js 22 及以上(见 package.json 的engines字段),使用 pnpm 工作区管理,属于 ES Module("type": "module")。
npm add @scalar/json-schema-validator基础用法:validate 一步到位
最直接的用法是把"文档"和"Schema"同时传给validate。文档可以是对象,也可以是 JSON / YAML 字符串:
import { validate } from '@scalar/json-schema-validator' const schema = { $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', required: ['name'], properties: { name: { type: 'string' } }, } const result = validate({ name: 'Hello' }, schema) console.log(result.valid) if (!result.valid) { console.log(result.errors) }返回的ValidationResult是判别联合类型(见 types.ts):
- 校验通过时:
{ valid: true, errors: [] }; - 校验失败时:
{ valid: false, errors: ErrorObject[] }。
其中ErrorObject形如{ message: string, path?: string | string[] }——path可能是 JSON Pointer 字符串(Schema 校验产生),也可能是一组路径段(调用方自行添加语义错误时使用)。测试用例 validate.test.ts 验证了典型行为:
- 缺必填字段时,错误消息形如
must have required property 'name'; - 出现未声明属性(
additionalProperties: false)时,消息为Property extra is not expected to be here; - 传入
'{ "name": "Hi" }'(JSON 字符串)与'name: Hi\n'(YAML 字符串)均可正常校验。
方言自动识别
不需要手动指定 JSON Schema 版本。校验器会从 Schema 的$schema字段自动选择方言,支持:
- draft-04:
http://json-schema.org/draft-04/schema - draft-07:
http://json-schema.org/draft-07/schema - 2020-12:
https://json-schema.org/draft/2020-12/schema
这一逻辑在 validate.ts 的ajvClassesByDialect映射中实现:draft-04 用ajv-draft-04包,2020-12 用ajv/dist/2020.js,其余默认回落到标准ajv。值得注意的一个细节是:映射的 key故意去掉了末尾的#,因为不同 Schema 对$schema的写法不一致(OpenAPI 的 draft-04 带#,AsyncAPI 的 draft-07 不带),查找时会对$schema值做同样的规范化,因此两种写法都能正确解析。
把 YAML 字符串也当作一等输入
validate内部对字符串输入统一走 YAML 解析(YAML 是 JSON 的超集,因此 JSON 字符串同样适用)。一个贴心的设计是:格式错误的字符串不会以解析器异常的形式逃逸,而是被当作一次普通的校验失败——除非显式开启了throwOnError。相关测试验证了validate('{ name: ', schema)返回valid: false且恰好一条错误,而开启throwOnError后则抛错。
复用 Schema:createValidator 一次性编译
当需要对大量文档使用同一个 Schema 时,反复编译 Schema 是很昂贵的。createValidator会把 Schema 编译一次,返回一个可反复调用的校验函数:
import { createValidator } from '@scalar/json-schema-validator' const validateUser = createValidator(schema) validateUser({ name: 'Ada' }) validateUser({ name: 'Grace' })从 validate.ts 的实现看,createValidator在调用时立即同步编译(compile直接执行),所以如果 Schema 本身无法编译(如非法的正则pattern),错误会从这个工厂函数直接抛出,而不是推迟到后续的校验调用——也就是说,这是"装配期失败"而非"运行期失败",validate.test.ts中专门有一条用例断言createValidator({ type: 'string', pattern: '(' })会立即toThrow()。
与之相对,validate对不可信 Schema更宽容:编译失败会被捕获并以valid: false的校验结果形式返回(详见下文"编译缓存"一节)。
validate 背后的编译缓存
validate内置了两张WeakMap缓存(validate.ts):
compiledValidators:按Schema 对象的引用身份缓存编译结果。同一个 Schema 对象再次传入时直接复用已编译的ValidateFunction,不会重复编译;failedCompilations:缓存"编译失败"的 Schema 及其失败原因,保证坏 Schema快速失败,而不是每次调用都重付一次完整的 Ajv 装配开销。
由此引出一个重要使用约束:formats只在某个 Schema 对象第一次被validate看到时生效。因为后续调用复用第一次编译出的校验器,即使你传入不同的formats也不会重新生效。如果需要同一 Schema 配不同 format 集合,请改用createValidator分别构建(见下一节)。
缓存还有两个边界情况值得说明:
- 布尔 Schema:JSON Schema 规范允许
true/false作为合法 Schema(true表示任意值通过,false表示全部拒绝),但布尔值无法作为WeakMap的 key,因此validate会跳过缓存直接编译(validate.test.ts 有用例覆盖); - 编译失败去重:同一坏 Schema 反复开启
throwOnError时,抛出的错误对象是缓存中记录的同一个实例,测试通过断言两次抛错引用相等(thrown[1] === thrown[0])来证明"未重新编译"。
注册扩展 format
Ajv 默认只认识部分内建 format,@scalar/json-schema-validator通过ajv-formats注册了标准 format 集,同时允许调用方通过formats选项补充自定义 format:
validate(document, schema, { formats: { 'media-range': true, }, })这里的值可以是true(仅做存在性检查,不校验格式内容)或一个 format 定义函数。实际实现中,compile会先调用addFormats(ajv)注册标准 format,再遍历调用方传入的formats逐项执行ajv.addFormat(name, definition)(见 validate.ts)。
一个来自 Scalar 生态的真实例子:OpenAPI 3.1 / 3.2 文档中的media-rangeformat(如Accept/Content-Type头值)就是通过这种方式补充注册的,这一点在 types.ts 的CreateValidatorOptions注释中有明确说明。
注意,formats属于编译期选项(CreateValidatorOptions),它影响的是 Schema 如何被编译成校验器,因此:
- 放在
createValidator(schema, { formats })上,每次新建校验器都会生效; - 放在
validate上,只有该 Schema 首次编译时生效(受缓存影响); - 不要期望同一个
validate调用序列里"换 formats 换行为"。
出错即抛:throwOnError
默认情况下validate永不抛出(对 Schema 校验、解析失败、编译失败一视同仁),所有失败都以valid: false结果返回。如果你希望"遇错即抛",可以开启throwOnError:
try { validate(document, schema, { throwOnError: true }) } catch (error) { // Handle the first validation error }throwOnError的语义在 types.ts 中标注默认值为false,并贯穿整个包的失败路径:
- Schema 校验失败时,抛出
errors[0]的消息文本构造的Error; - 文档字符串解析失败时,抛出解析器原始异常;
- Schema 编译失败时,重新抛出缓存的编译错误;
createSpecificationValidator场景下,emptyOrInvalid、versionNotSupported等前置失败同样遵循此开关。
值得注意的是,throwOnError抛出的"第一个错误"是美化、去重后的第一条错误,而不是 Ajv 的原始第一条——这保证了异常信息同样可读。
错误处理流水线:从 Ajv 原始错误到友好消息
包内错误处理遵循一条清晰的三段式流水线(transform-errors.ts):
transformErrors → prettifyAjvErrors → deduplicateErrorstransformErrors:入口封装。若输入本身是字符串则直接作为单条错误返回;若文档非法(如$ref解析失败导致文档为空或非对象),返回Invalid specification;否则调用prettifyAjvErrors并 trim 每条消息。由于畸形 Schema 可能导致美化过程自身崩溃,这里包了try/catch,失败时回退到原始 Ajv 错误(对additionalProperties错误补充属性名)。prettifyAjvErrors(prettify-ajv-errors.ts):核心美化逻辑,分三步:- 建树:把所有扁平错误按 JSON Pointer 的每个
/segment组织成错误树。正则JSON_POINTERS_REGEX刻意用[^/]+而非单词字符,从而保证被转义的指针段(如/paths/~1pets)和带点的 key(如/pet.store)各自独立成节点,不会因折叠而互相错误剪枝; - 剪枝:按"错误具体程度"排序去冗余——
oneOf/anyOf/if这类"容器错误"只说明分支失败、不说明原因,因此让位于更具体的错误;required是"具体且终结"的(缺必填属性使该对象其余错误失去意义,直接胜出);enum具体但较弱,让位于同层更具体的错误;其余(type/pattern/format等)一律保留。OpenAPI 中典型的oneOf: [Schema, Reference]联合导致的oneOf+required纠缠,会优先保留"用户最可能想看到的"required分支错误,仅当唯一缺失属性是$ref(即值看起来像坏引用)时才回落为泛化的oneOf错误; - 成文:把过滤后的树再拉平成扁平列表,按关键字逐条措辞。例如:
additionalProperties/unevaluatedProperties:Property xxx is not expected to be here;pattern:Property "name" must match pattern ^[a-z]+$(能从文档解析出属性名时);required:沿用 Ajv 原生消息must have required property 'name';format:对$ref上的uri-reference专门增强——当$ref含非 ASCII 字符时输出$ref "xxx" contains non-ASCII characters,否则输出$ref "xxx" is not a valid URI reference;- 同一节点的多个
enum错误会合并为一条,列出所有允许值。
测试 prettify-ajv-errors.test.ts 验证了这些行为,例如
Property "404" must match pattern ^[a-z]+$(OpenAPI 中responses.404这类数字对象键不会被误判为数组下标)、/日本語这类非 ASCII 指针路径的保留,以及/paths/~1a/get、/paths/~1b/post各自独立成节点。- 建树:把所有扁平错误按 JSON Pointer 的每个
deduplicateErrors(deduplicate-errors.ts):以消息||路径为 key 去除 message 与 path 完全相同的重复错误。path 为数组时先join('.')归一化,因此 JSON Pointer 字符串与路径段数组两种形态可以统一比较。
更高一层的封装:createSpecificationValidator
虽然本文主角是"纯 JSON Schema 校验",但包内还提供了一个面向规范文档的抽象——createSpecificationValidator(create-specification-validator.ts),它是@scalar/openapi-validator等上层包与共享内核之间的桥梁,也最能体现本包"引擎中立"的设计。
它接收一个SpecificationValidatorConfig,把"各版本 Schema、版本探测、扩展 format、前置文档调整、后置语义检查"全部收口:
type SpecificationValidatorConfig<TVersion, TOptions> = { schemas: Record<TVersion, SchemaObject> // 每个受支持版本的 JSON Schema detectVersion: (document) => TVersion | undefined // 从解析后的文档探测规范版本 formats?: (version) => Record<string, unknown> // 按版本注册额外 format errors: { emptyOrInvalid: string; versionNotSupported: string } prepareDocument?: (specification, version) => AnyObject // 校验前调整文档(不修改调用方副本) postValidate?: (specification, version, options) => ErrorObject[] // Schema 通过后的语义检查 }返回的校验函数执行固定流程:解析字符串 → 拒绝非对象文档(emptyOrInvalid)→ 探测版本(未识别则versionNotSupported)→ 按版本编译并缓存校验器(每个版本只编译一次、跨调用复用)→ 可选prepareDocument→ Schema 校验 → 可选postValidate语义检查。校验结果类型ValidationOutcome额外携带version与schema,方便上层报告"哪个版本的规范、基于什么 Schema 得出结论"。
从源码结构可以推断,这种"规范无关内核 + 规范相关配置"的分层,正是 Scalar 能同时支撑 OpenAPI 与 AsyncAPI 校验、并让两者共享同一套错误美化能力的关键架构选择。
小结
@scalar/json-schema-validator是一个小而专的通用 JSON Schema 校验引擎:以 Ajv 为内核,自动识别 draft-04 / draft-07 / 2020-12 方言,接受对象或 JSON/YAML 字符串输入,通过编译缓存与失败缓存兼顾性能与快速失败,最终用"建树—剪枝—成文—去重"的流水线把 Ajv 的原始错误打磨成一句句可读、可定位、无冗余的人类友好消息。无论你是要在自己的工具链里直接用它校验数据,还是想理解 Scalar 的 OpenAPI / AsyncAPI 校验器为什么能输出那么干净的报错,validate.ts、prettify-ajv-errors.ts 与配套测试都是极佳的研读入口。
【免费下载链接】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),仅供参考