validate() 函数支持 DSL 对象说明
问题
用户问:validate(schema, { email: 'test@example.com', age: 25 }) 中的 schema 能否直接是个对象,为什么必须是 schema?
答案
现在可以了! 🎉 当前 TypeScript 重构版中,顶层 validate() 和 validateAsync() 都支持直接传入 DSL 对象。
支持的三种方式
方式1:传入 DSL 对象(✅ 当前版本支持)
const { validate } = require('schema-dsl');
// ✅ 直接传入 DSL 对象,无需 dsl() 包裹
const result = validate(
{ email: 'email!', age: 'number:18-120' }, // DSL 对象
{ email: 'test@example.com', age: 25 }
);
console.log(result.valid); // true
优点:
- ✅ 最简洁,无需
dsl() 包裹
- ✅ 代码更直观,适合简单场景
⚠️ 注意:DSL 对象也支持混合使用 DslBuilder 实例:
const { dsl, validate } = require('schema-dsl');
// ✅ 混合使用:DslBuilder + DSL 字符串
const result = validate(
{
username: dsl('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.messages({ 'string.pattern': '只能包含字母、数字和下划线' }),
email: 'email!', // 纯 DSL 字符串
age: 'number:18-'
},
data
);
方式2:使用 dsl() 包裹(推荐)
const { dsl, validate } = require('schema-dsl');
// ✅ 先转换为 JSON Schema,再验证
const schema = dsl({
email: 'email!',
age: 'number:18-120'
});
const result = validate(schema, { email: 'test@example.com', age: 25 });
优点:
- ✅ 更明确,意图清晰
- ✅ 可复用 schema
- ✅ 支持链式调用扩展
方式3:传入标准 JSON Schema
const { validate } = require('schema-dsl');
// ✅ 传入标准 JSON Schema
const result = validate(
{
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
age: { type: 'number', minimum: 18, maximum: 120 }
},
required: ['email']
},
{ email: 'test@example.com', age: 25 }
);
优点:
- ✅ 兼容标准 JSON Schema
- ✅ 可与其他 JSON Schema 工具互操作
实现原理
自动检测逻辑
顶层 validate() / validateAsync() 会先归一化传入的 schema:
function validate(schema, data, options = {}) {
const normalizedSchema = _normalizeSchemaInput(schema);
const validator = getDefaultValidator();
return validator.validate(normalizedSchema, data, options);
}
检测规则
判断是否为 DSL 对象的逻辑(_isDslObject()):
- 排除非对象:不是普通对象返回 false
- 排除 DslBuilder:有
toSchema() 方法返回 false
- 排除 ConditionalBuilder:有
_isConditional 标记返回 false
- 排除标准 JSON Schema:
- 有
type 字段且值为标准类型(string/number/object等)
properties 的所有值都包含 type 字段
- 识别 DSL 对象:
- 属性值包含 DSL 字符串(如
'email!', 'string:3-32')
- 属性值包含嵌套的 DSL 对象
为什么之前必须是 schema?
背景
早期实现中,顶层 validate() 不会自动转换 DSL 对象:
// ❌ v1.1.6 及之前版本会失败
const result = validate(
{ email: 'email!', age: 'number!' }, // 被当作 JSON Schema
{ email: 'test@example.com', age: 25 }
);
// 错误:Schema compilation failed: unknown keyword: "email"
原因:validate() 会把 DSL 对象当作标准 JSON Schema,而 "email!" 不是有效的 JSON Schema 关键字。
当前方案
当前 TypeScript 重构版已补齐自动检测和转换逻辑:
- 检测 DSL 对象:识别对象中的 DSL 字符串
- 自动转换:调用
DslAdapter.parseObject() 转换为 JSON Schema
- 透明处理:用户无需关心内部转换
使用建议
简单场景:直接用 DSL 对象
适用于:脚本、原型开发、测试代码、一次性验证
// ✅ 简单验证,直接传 DSL 对象
app.post('/api/user', (req, res) => {
const result = validate(
{ email: 'email!', age: 'number:18-' },
req.body
);
if (!result.valid) {
return res.status(400).json({ errors: result.errors });
}
// 处理数据...
});
复杂场景:项目启动时配置 schema(推荐)
适用于:生产环境、高并发服务、需要复用的场景
// ✅ 最佳实践:在单独的文件中定义所有 schema
// schemas/user.js - 项目启动时加载,转换一次
const { dsl } = require('schema-dsl');
module.exports = {
register: dsl({
username: dsl('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.messages({ 'string.pattern': '只能包含字母、数字和下划线' }),
email: 'email!',
password: dsl('string!').password('strong'),
age: 'number:18-120'
}),
login: dsl({
username: 'string!',
password: 'string!'
}),
updateProfile: dsl({
nickname: 'string:2-20',
avatar: 'url',
bio: 'string:0-500'
})
};
// routes/user.js - 路由中直接使用,不再转换
const userSchemas = require('../schemas/user');
const { validate } = require('schema-dsl');
app.post('/api/register', (req, res) => {
const result = validate(userSchemas.register, req.body); // ✅ 直接使用
// ...
});
app.post('/api/login', (req, res) => {
const result = validate(userSchemas.login, req.body); // ✅ 直接使用
// ...
});
app.put('/api/user/profile', (req, res) => {
const result = validate(userSchemas.updateProfile, req.body); // ✅ 直接使用
// ...
});
性能优势:
- ✅ 避免每次请求都转换 DSL 对象
- ✅ schema 只在项目启动时创建一次
- ✅ 适合高并发场景
需要链式调用:混合使用 DslBuilder
适用于:需要自定义错误消息、复杂验证规则
// ✅ 需要自定义消息
const schema = dsl({
email: dsl('email!')
.label('邮箱地址')
.messages({ 'string.email': '请输入有效的邮箱' }),
username: dsl('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.messages({ 'string.pattern': '只能包含字母、数字和下划线' })
});
const result = validate(schema, data);
对比总结
注意事项
1. 性能考虑
DSL 对象会在每次 validate() 调用时转换,如果需要高性能:
// ❌ 不推荐:每次请求都转换
app.post('/api/user', (req, res) => {
const result = validate(
{ email: 'email!', age: 'number!' }, // 每次都转换
req.body
);
});
// ✅ 推荐:提前转换,复用 schema
const userSchema = dsl({ email: 'email!', age: 'number!' });
app.post('/api/user', (req, res) => {
const result = validate(userSchema, req.body); // 直接使用
});
2. 类型混淆
确保 DSL 对象不会被误识别为 JSON Schema:
// ✅ 明确的 DSL 对象
{ email: 'email!', age: 'number!' } // 自动识别
// ⚠️ 可能混淆
{
type: 'object', // 有 type 字段
email: 'email!' // 但还有 DSL 字符串
}
// 会被识别为 JSON Schema(type 优先级高)
3. 嵌套对象
嵌套的 DSL 对象会被正确处理:
// ✅ 支持嵌套
const result = validate(
{
user: {
profile: {
name: 'string!',
age: 'number!'
}
}
},
data
);
完整示例
const { dsl, validate, validateAsync } = require('schema-dsl');
// 示例1:同步验证
const result = validate(
{
email: 'email!',
password: dsl('string!').password('strong'),
age: 'number:18-120',
username: 'string:3-32!'
},
{
email: 'test@example.com',
password: 'MyP@ssw0rd!',
age: 25,
username: 'john_doe'
}
);
if (result.valid) {
console.log('验证通过');
} else {
console.log('验证失败:', result.errors);
}
// 示例2:异步验证
(async () => {
try {
const data = await validateAsync(
{ email: 'email!', age: 'number!' },
{ email: 'test@example.com', age: 25 }
);
console.log('验证通过:', data);
} catch (error) {
console.error('验证失败:', error.errors);
}
})();
总结
问:为什么必须是 schema?
答:现在不必了!
- ✅ 当前版本支持直接传入 DSL 对象
- ✅ 自动检测并转换,无需手动包裹
- ✅ 完全向后兼容,不影响原有功能
- ✅ 同时支持 JSON Schema、DslBuilder、DSL 对象三种方式
推荐使用:
- 简单场景:直接用 DSL 对象
- 复杂场景:先用
dsl() 转换,便于复用和扩展
常见问题
Q1: DSL 对象中可以使用链式调用吗?
A: 可以! 支持混合使用 DslBuilder 实例和 DSL 字符串:
const result = validate(
{
username: dsl('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.messages({ 'string.pattern': '只能包含字母、数字和下划线' }),
email: 'email!', // 纯 DSL 字符串
age: 'number:18-'
},
data
);
嵌套对象中也支持:
const result = validate(
{
user: {
name: dsl('string:3-32!').messages({ 'string.min': '名字太短了' }),
email: 'email!'
}
},
data
);
Q2: 直接用对象会有什么影响?
性能影响:
每次调用 validate() 时,DSL 对象都会被转换为 JSON Schema:
// ❌ 性能较差:每次请求都重复转换
app.post('/api/user', (req, res) => {
const result = validate(
{ email: 'email!', age: 'number!' }, // ❌ 每次请求都会执行 DSL → JSON Schema 转换
req.body
);
});
// ✅ 性能最优:项目启动时转换一次,复用 schema
const userSchema = dsl({ email: 'email!', age: 'number!' }); // ✅ 启动时转换一次
app.post('/api/user', (req, res) => {
const result = validate(userSchema, req.body); // ✅ 直接使用,不再转换
});
ℹ️ 具体耗时取决于机器性能、Node 版本、schema 复杂度和命中率;这里强调的是“预先转换后复用通常显著快于每次请求都重新转换”的相对结论,而不是固定秒数。
性能差异:约 3-5%(对于简单 schema)
✅ 您的理解完全正确!
最佳实践:在项目启动时配置好所有 schema
// ✅ 推荐:在单独的文件中定义所有 schema(schemas/user.js)
const { dsl } = require('schema-dsl');
// 项目启动时转换一次,后续直接复用
const userSchemas = {
register: dsl({
username: dsl('string:3-32!')
.pattern(/^[a-zA-Z0-9_]+$/)
.messages({ 'string.pattern': '只能包含字母、数字和下划线' }),
email: 'email!',
password: dsl('string!').password('strong'),
age: 'number:18-120'
}),
login: dsl({
username: 'string!',
password: 'string!'
}),
updateProfile: dsl({
nickname: 'string:2-20',
avatar: 'url',
bio: 'string:0-500'
})
};
module.exports = userSchemas;
// 在路由中使用(routes/user.js)
const userSchemas = require('../schemas/user');
app.post('/api/register', (req, res) => {
const result = validate(userSchemas.register, req.body); // ✅ 直接使用
// ...
});
app.post('/api/login', (req, res) => {
const result = validate(userSchemas.login, req.body); // ✅ 直接使用
// ...
});
场景建议:
Q3: 为什么复杂场景仍然建议先用 dsl() 转换?
历史原因:
-
明确的职责分离(设计哲学)
// 转换阶段:DSL → JSON Schema
const schema = dsl({ email: 'email!', age: 'number!' });
// 验证阶段:JSON Schema + data → result
const result = validate(schema, data);
这种设计让每个步骤的职责更清晰。
-
避免在高频路径里滥用隐式转换(最小惊喜原则)
// 用户传入什么,就是什么
validate(jsonSchema, data); // JSON Schema
validate(dslBuilder, data); // DslBuilder
// ⚠️ 当前虽然支持隐式转换,但高频场景仍建议预先转换后复用
validate({ email: 'email!' }, data);
-
类型安全考虑(TypeScript)
// 明确的类型定义
function validate(
schema: JSONSchema | DslBuilder, // 明确的类型
data: any
): ValidationResult;
// 如果支持任意对象,类型推断会变复杂
function validate(
schema: JSONSchema | DslBuilder | Record<string, any>, // 太宽泛
data: any
): ValidationResult;
-
性能考虑(避免重复转换)
// 避免用户不经意间写出性能差的代码
for (let i = 0; i < 10000; i++) {
validate({ email: 'email!' }, data); // 每次都转换
}
为什么当前版本要补齐这个能力?
- 用户反馈:很多用户期望更简洁的 API
- 智能检测:通过
_isDslObject() 准确区分 DSL 对象和 JSON Schema
- 性能可接受:转换开销很小(~3-5%)
- 向后兼容:不影响现有代码
- 使用体验优先:简化常见场景的使用
设计权衡:
最终选择:两者都支持,让用户自由选择!
// ✅ 简单场景:直接用 DSL 对象
validate({ email: 'email!' }, data);
// ✅ 复杂场景:显式转换
const schema = dsl({ email: 'email!' });
validate(schema, data);
对应示例文件
示例入口: validate-dsl-object-support.ts
说明: 覆盖直接传入 DSL 对象、混合使用 DslBuilder 与 DSL 字符串,以及顶层 validate() / validateAsync() 的真实支持边界。