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无关,但会影响后续的数据验证。
备注:内容仅供参考。
