基于 Ajv 的通用 JSON Schema 校验引擎:@scalar/json-schema-validator 使用指南与源码解析
2026/9/15 22:45:50 网站建设 项目流程

基于 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 的导出可以看到,这个包对外暴露了四组能力:

  1. validate/createValidator:面向任意 JSON Schema 的通用校验 API(来自 validate.ts);
  2. createSpecificationValidator/SpecificationValidatorConfig/SpecificationValidatorOptions:面向 OpenAPI、AsyncAPI 等"规范文档"的高层封装器(来自 create-specification-validator.ts);
  3. prettifyAjvErrors/AjvError/PrettyError:错误美化工具(来自 prettify-ajv-errors.ts);
  4. 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-04http://json-schema.org/draft-04/schema
  • draft-07http://json-schema.org/draft-07/schema
  • 2020-12https://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场景下,emptyOrInvalidversionNotSupported等前置失败同样遵循此开关。

值得注意的是,throwOnError抛出的"第一个错误"是美化、去重后的第一条错误,而不是 Ajv 的原始第一条——这保证了异常信息同样可读。

错误处理流水线:从 Ajv 原始错误到友好消息

包内错误处理遵循一条清晰的三段式流水线(transform-errors.ts):

transformErrors → prettifyAjvErrors → deduplicateErrors
  1. transformErrors:入口封装。若输入本身是字符串则直接作为单条错误返回;若文档非法(如$ref解析失败导致文档为空或非对象),返回Invalid specification;否则调用prettifyAjvErrors并 trim 每条消息。由于畸形 Schema 可能导致美化过程自身崩溃,这里包了try/catch,失败时回退到原始 Ajv 错误(对additionalProperties错误补充属性名)。

  2. 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/unevaluatedPropertiesProperty xxx is not expected to be here
      • patternProperty "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各自独立成节点。

  3. 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额外携带versionschema,方便上层报告"哪个版本的规范、基于什么 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询