
本文介绍如何利用 Joi 的条件验证(.when())机制,实现“Latitude/Longitude 二者任一提供即跳过地址字段校验,否则 AddressLine、City、PostalCode 必须全部非空”的业务规则。
本文介绍如何利用 joi 的条件验证(`.when()`)机制,实现“latitude/longitude 二者任一提供即跳过地址字段校验,否则 addressline、city、postalcode 必须全部非空”的业务规则。
在构建 API 请求校验逻辑时,常遇到“二选一”或“互斥必填”的复合验证需求。例如本例中的地址对象:系统既支持通过经纬度(Latitude/Longitude)精确定位,也支持通过结构化文本地址(AddressLine、City、PostalCode)模糊匹配。业务规则明确要求:只要 Latitude 或 Longitude 中至少一个为有效数值(非 null、非 undefined、非空字符串),则地址三字段可选;否则三者必须全部存在且非空。
Joi 并不直接支持“多字段联合条件判断”,但可通过嵌套 .when() 实现等效逻辑。核心思路是:以 Longitude 为第一判据,若其为 null,则强制校验地址字段;若其非 null,再进一步检查 Latitude —— 若 Latitude 也为 null,仍需地址字段;仅当二者均非 null 时,地址字段才可省略。
以下是完整、健壮的 Joi 校验 Schema 示例:
const Joi = require('joi');
const addressSchema = Joi.object({
Longitude: Joi.number().allow(null, '').optional(),
Latitude: Joi.number().allow(null, '').optional(),
AddressLine: Joi.string()
.when('Longitude', {
is: Joi.exist().not(null).not(''),
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string()
.when('Latitude', {
is: Joi.exist().not(null).not(''),
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string().required().min(1).label('AddressLine')
})
})
.label('AddressLine'),
City: Joi.string()
.when('Longitude', {
is: Joi.exist().not(null).not(''),
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string()
.when('Latitude', {
is: Joi.exist().not(null).not(''),
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string().required().min(1).label('City')
})
})
.label('City'),
PostalCode: Joi.string()
.when('Longitude', {
is: Joi.exist().not(null).not(''),
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string()
.when('Latitude', {
is: Joi.exist().not(null).not(''),
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string().required().min(1).label('PostalCode')
})
})
.label('PostalCode'),
}).unknown(true);
const dataSchema = Joi.object({
Id: Joi.string().uuid().required().label('Task ID'),
Address: addressSchema,
}).unknown(true);
function validateData(data) {
return dataSchema.validate(data, { abortEarly: false });
}
module.exports = { validateData };✅ 关键说明与最佳实践:
- 使用
Joi.exist().not(null).not('')替代简单的is: null,确保能正确识别0、false等 falsy 但合法的数值(Latitude/Longitude可为0); - 显式调用
.allow(null, '')避免空字符串被默认拒绝; - 地址字段在“必填分支”中添加
.min(1),防止仅传入空白字符串; - 启用
{ abortEarly: false }可一次性返回所有校验错误,提升调试效率; - 将
AddressSchema 单独提取为常量,增强可读性与复用性。
⚠️ 注意事项:
- Joi v17+ 中
Joi.forbidden()不适用于条件分支的then/otherwise,应改用Joi.optional().allow(null, '')或Joi.any().valid(null).optional(); - 若业务允许
Latitude/Longitude为字符串形式(如"40.7128"),需配合.pattern()或自定义转换; - 前端提交时建议统一将空经纬度设为
null,避免因字符串"null"或空格导致校验失效。
通过上述方案,你既能精准满足“经纬度优先、地址兜底”的业务语义,又能保持校验逻辑清晰、可维护、可测试。

















