PAYATHON 2026

Ajv 编译 JSON Schema 类型定义时出现 MissingRefError,如何解决?

排障赵师傅

先将完整 Schema 注册到 Ajv,再通过“Schema 标识符 + 定义路径”分别编译三个类型的验证函数。直接把 schema.definitions.SetUserAccountDataPayload 传给 ajv.compile(),会丢失根 Schema 的上下文,导致内部的 $ref 无法解析。

为什么会出现 MissingRefError?

SetUserAccountDataPayload 中包含以下引用:

{
  "data": {
    "$ref": "#/definitions/UserAccountData"
  }
}

题目中的 Schema 没有嵌套的 $id 改变解析范围,因此 #/definitions/UserAccountData 指向根 Schema 下的 definitions.UserAccountData。

如果直接执行以下代码:

ajv.compile(schema.definitions.SetUserAccountDataPayload);

Ajv 会把传入的子对象当作独立的根 Schema。这个对象只有 type、properties、required 等字段,没有 definitions.UserAccountData,因此会抛出错误:

MissingRefError: can't resolve reference #/definitions/UserAccountData from id #

通过 JavaScript 对象属性取出的子对象不会保留 JSON Schema 的父级上下文。即使它来自完整 Schema,Ajv 也无法据此找到它的兄弟定义。

UpdateUserAccountDataPayload 也依赖其他定义,会遇到同样的问题。RemoveUserAccountDataPayload 没有 $ref,单独编译通常可以成功。

推荐做法:注册完整 Schema,再编译引用

用 addSchema() 为完整 Schema 注册一个标识符,再让各个验证函数引用对应的定义。

下面沿用问题中的 TypeScript 类型,使用前需要在当前文件中定义或导入这些类型:

import Ajv from 'ajv';
import schema from 'app/features/cross-platform-bridge/types/cross-platform-bridge.types.json';

const ajv = new Ajv();

const schemaKey =
  'https://example.com/schemas/cross-platform-bridge.json';

ajv.addSchema(schema, schemaKey);

const validate1 = ajv.compile<SetUserAccountDataPayload>({
  $ref: `${schemaKey}#/definitions/SetUserAccountDataPayload`,
});

const validate2 = ajv.compile<UpdateUserAccountDataPayload>({
  $ref: `${schemaKey}#/definitions/UpdateUserAccountDataPayload`,
});

const validate3 = ajv.compile<RemoveUserAccountDataPayload>({
  $ref: `${schemaKey}#/definitions/RemoveUserAccountDataPayload`,
});

这里的 URL 是 Schema 标识符。完整 Schema 已通过 addSchema() 注册到同一个 Ajv 实例中,因此解析这些引用时不需要请求该 URL。

编译 SetUserAccountDataPayload 时,Ajv 就能在已注册的完整 Schema 中找到 UserAccountData,以及它引用的 VerificationStatus 和 SessionStatus。

创建验证函数后,传入单个 payload 即可:

const payload: unknown = {
  data: null,
  type: 'removeUserAccountData',
  version: '1',
};

if (!validate3(payload)) {
  console.error(validate3.errors);
}

按题目中的定义,type 和 version 只要求是字符串,不限制具体取值;data 必须存在,且值为 null。

也可以通过 getSchema() 按完整引用获取对应的验证函数,但需要处理返回值为 undefined 的情况:

const validateSet = ajv.getSchema<SetUserAccountDataPayload>(
  `${schemaKey}#/definitions/SetUserAccountDataPayload`,
);

if (!validateSet) {
  throw new Error('SetUserAccountDataPayload schema not found');
}

另一种做法:编译时保留完整 definitions

如果不需要集中注册 Schema,也可以为每个入口创建一个包装 Schema,将完整的 definitions 一并传入:

const validate1 = ajv.compile<SetUserAccountDataPayload>({
  $schema: schema.$schema,
  definitions: schema.definitions,
  $ref: '#/definitions/SetUserAccountDataPayload',
});

const validate2 = ajv.compile<UpdateUserAccountDataPayload>({
  $schema: schema.$schema,
  definitions: schema.definitions,
  $ref: '#/definitions/UpdateUserAccountDataPayload',
});

const validate3 = ajv.compile<RemoveUserAccountDataPayload>({
  $schema: schema.$schema,
  definitions: schema.definitions,
  $ref: '#/definitions/RemoveUserAccountDataPayload',
});

每个包装 Schema 都包含完整的定义集合,内部的本地引用因此可以正常解析。题目中的 Schema 没有 $id,这种方式也能保留所需的引用上下文。

使用时需要注意什么?

  • 直接编译完整 Schema 会验证 SchemaRoot。原始顶层 $ref 指向 #/definitions/SchemaRoot,其中的 required 要求三个 payload 字段同时存在。要验证单个 payload,应引用对应的定义。
  • 注册完整 Schema 后,直接抽取子对象编译仍会遇到引用问题,因为原来的 #/definitions/... 仍是本地引用。应使用带注册标识符的 $ref,或采用保留完整 definitions 的包装方式。
  • 定义名称中的 URI 编码需要保留。AtLeastOne%3CUserAccountData%3E 中的 %3C 和 %3E 分别表示 < 和 >,对应定义名 AtLeastOne<UserAccountData>,这本身不是报错原因。
  • TypeScript 泛型不会补齐引用。compile<SetUserAccountDataPayload>() 提供类型信息,运行时的验证规则仍由传入的 JSON Schema 决定。
  • "null" 和 null 不同。当前 SessionStatus 允许字符串 "null",不允许 JSON 值 null。这与 MissingRefError 无关,但会影响后续的数据验证。

备注:内容仅供参考。