Ajv 是 Node.js 生态中使用最广泛的 JSON Schema 校验器之一,很多接口层的数据验证都依赖它完成。当 schema 中出现 "format": "uri" 这样的约束时,开发者常常会遇到一个困惑:明明传入的是 https://ippipp.com 这样看起来完全正常的字符串,校验却报错;而某些带中文或特殊字符的地址反而能通过。这背后的原因在于 Ajv 对 URI 的验证并非简单的字符串匹配,而是基于 RFC3986 规范实现的完整解析逻辑。要真正掌握这个行为,需要从规范本身、Ajv 的实现方式以及配置选项三个层面来理解。

一、RFC3986 定义的 URI 到底是什么
很多人把 URI 等同于网址,这是一个常见误解。RFC3986 明确定义了 URI 的语法结构,它由 scheme、authority、path、query 和 fragment 五个部分组成,通用格式为 scheme://authority/path?query#fragment。关键点在于:规范要求的首先是合法性,而不仅仅是常见性。
举个典型的例子,mailto:someone@ippipp.com 和 urn:isbn:0451450523 都是合法的 URI,但它们没有 // 也不会被浏览器当作网址打开。反过来,www.ippipp.com 这种缺少 scheme 的字符串,虽然人眼一看就知道是什么,却不符合 RFC3986 对 URI 的定义,因此 Ajv 会直接判定校验失败。
另一个容易踩坑的地方是字符集。RFC3986 规定 URI 中的非保留字符和保留字符有明确范围,中文字符、空格等必须经过百分号编码。所以 https://ippipp.com/页面 在 Ajv 的 uri format 校验下会失败,必须写成 https://ippipp.com/%E9%A1%B5%E9%9D%A2 才能通过。这个设计并非 Ajv 故意刁难,而是严格遵循了规范要求。
二、Ajv 中的 uri 与 uri-reference 有何区别
Ajv 支持多个与 URI 相关的 format,理解它们的差异是正确使用的前提。最常用的两个是 uri 和 uri-reference。
uri 要求字符串必须是一个完整的 URI,也就是必须包含 scheme 部分。而 uri-reference 则更宽松,它允许相对引用,比如 /api/users、../images/logo.png 这样的相对路径也能通过校验。如果你的数据中存的是站内相对路径,却错误地使用了 uri format,就会莫名其妙地校验失败。
const Ajv = require("ajv");
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
homepage: { type: "string", format: "uri" },
avatar: { type: "string", format: "uri-reference" }
}
};
const validate = ajv.compile(schema);
// homepage 校验通过
console.log(validate({
homepage: "https://ipipp.com",
avatar: "/img/avatar.png"
}));
// homepage 校验失败,因为缺少 scheme
console.log(validate({
homepage: "ipipp.com",
avatar: "/img/avatar.png"
}));
// 输出失败详情
if (!validate.errors) return;
console.log(validate.errors.map(e => e.message).join("\n"));
除了这两个之外,Ajv 还提供 iri 和 iri-reference,对应 RFC3987 中的国际化资源标识符,允许使用更宽泛的 Unicode 字符。如果你的业务确实需要接受包含中文的地址字符串,考虑使用 iri 系列 format 或者提前做编码处理会更合适。
三、默认不开format校验与 fast-uri 性能方案
一个经常被忽略的坑是:Ajv 在默认配置下并不校验 format。很多开发者写了 format 约束,测试时却发现任何字符串都能通过,误以为 Ajv 的 URI 验证形同虚设。实际上这来自 JSON Schema 规范本身的设计——format 最初就是可选的注解性关键字,Ajv 6 之后的版本默认将其视为注解而非断言。
要开启校验,需要在构造实例时显式配置:
const Ajv = require("ajv");
const ajv = new Ajv({ format: "fast" });
// format: "fast" 使用正则快速校验,性能最好
// format: "full" 使用完整解析,校验更严格但更慢
// 对于 URI 场景,还可以引入 fast-uri 提升性能
const fastUri = require("fast-uri");
const ajvWithFastUri = new Ajv({ formats: { uri: fastUri } });
这里提到的 fast-uri 是社区提供的高性能替代方案,它比 Ajv 内置的正则快数倍,特别适合校验大量含 URI 字段的数据流场景。不过要注意,fast 的代价是某些边界情况下的判定差异,如果业务对严格性要求极高,建议使用 full 模式并做好性能压测。
另外在 Ajv 8 中引入的 strict mode 会在编译 schema 时做更严格的检查,如果传入未知的 format 名称会直接抛出异常。这对于排查拼写错误很有帮助,比如把 uri-reference 误写成 urireference,旧版本可能静默忽略,新版本则会在编译期就给出明确报错。
四、常见报错场景的排查思路
实际项目中遇到 URI 校验失败,可以按下面的顺序逐项排查,能覆盖绝大多数情况。
- 检查是否缺少 scheme:字符串是否以
xxx:开头,没有 scheme 的一律不是合法 URI。 - 检查非法字符:空格、中文、未编码的特殊符号都会导致失败,尝试用
encodeURI或encodeURIComponent预处理。 - 确认 format 名称与业务匹配:相对路径应使用 uri-reference,不要硬套 uri。
- 确认 format 校验已开启:检查 Ajv 构造参数中是否配置了 format 选项,避免约束根本没生效。
- 阅读错误信息中的 params:Ajv 的错误对象里 format 字段会标明是哪个 format 校验失败,比笼统的 must match format 有用得多。
还有一种特殊情况值得注意:空字符串。按照 RFC3986,空字符串不属于合法 URI,但如果 schema 中该字段是可选的,Ajv 会先执行 required 检查,optional 且值为空的场景会触发 format 报错而非类型报错。解决办法通常是在 schema 中组合 minLength 约束,或者在业务层对空值做前置过滤。
最后,如果团队对 URI 的要求本来就比 RFC 宽松或严格,完全可以自定义 format。Ajv 提供了 addFormat 接口,注册一个自己的验证函数即可,例如只接受 http 和 https 开头的地址。这样既保留了 schema 声明式的表达能力,又贴合实际业务规则,是大型项目中最灵活的处理方式。