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()):

  1. 排除非对象:不是普通对象返回 false
  2. 排除 DslBuilder:有 toSchema() 方法返回 false
  3. 排除 ConditionalBuilder:有 _isConditional 标记返回 false
  4. 排除标准 JSON Schema
    • type 字段且值为标准类型(string/number/object等)
    • properties 的所有值都包含 type 字段
  5. 识别 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 重构版已补齐自动检测和转换逻辑:

  1. 检测 DSL 对象:识别对象中的 DSL 字符串
  2. 自动转换:调用 DslAdapter.parseObject() 转换为 JSON Schema
  3. 透明处理:用户无需关心内部转换

使用建议

简单场景:直接用 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);

对比总结

方式简洁性灵活性复用性适用场景
DSL 对象⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐简单验证、一次性使用
dsl() 包裹⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐复杂验证、需要复用
JSON Schema⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐与其他工具互操作

注意事项

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);  // ✅ 直接使用
  // ...
});

场景建议

场景推荐方式原因
生产环境 API✅ 项目启动时配置 schema避免每次请求都转换,性能最优
高并发服务✅ 项目启动时配置 schema3-5% 的性能损失会被放大
单次脚本✅ 直接用 DSL 对象只执行一次,性能影响可忽略
原型开发✅ 直接用 DSL 对象快速迭代,无需在意性能
测试代码✅ 直接用 DSL 对象简洁清晰,易于维护

Q3: 为什么复杂场景仍然建议先用 dsl() 转换?

历史原因

  1. 明确的职责分离(设计哲学)

    // 转换阶段:DSL → JSON Schema
    const schema = dsl({ email: 'email!', age: 'number!' });
    
    // 验证阶段:JSON Schema + data → result
    const result = validate(schema, data);

    这种设计让每个步骤的职责更清晰。

  2. 避免在高频路径里滥用隐式转换(最小惊喜原则)

    // 用户传入什么,就是什么
    validate(jsonSchema, data);  // JSON Schema
    validate(dslBuilder, data);  // DslBuilder
    
     // ⚠️ 当前虽然支持隐式转换,但高频场景仍建议预先转换后复用
     validate({ email: 'email!' }, data);
  3. 类型安全考虑(TypeScript)

    // 明确的类型定义
    function validate(
      schema: JSONSchema | DslBuilder,  // 明确的类型
      data: any
    ): ValidationResult;
    
    // 如果支持任意对象,类型推断会变复杂
    function validate(
      schema: JSONSchema | DslBuilder | Record<string, any>,  // 太宽泛
      data: any
    ): ValidationResult;
  4. 性能考虑(避免重复转换)

    // 避免用户不经意间写出性能差的代码
    for (let i = 0; i < 10000; i++) {
      validate({ email: 'email!' }, data);  // 每次都转换
    }

为什么当前版本要补齐这个能力?

  1. 用户反馈:很多用户期望更简洁的 API
  2. 智能检测:通过 _isDslObject() 准确区分 DSL 对象和 JSON Schema
  3. 性能可接受:转换开销很小(~3-5%)
  4. 向后兼容:不影响现有代码
  5. 使用体验优先:简化常见场景的使用

设计权衡

设计方案优点缺点
显式转换职责清晰、类型安全、性能最优代码稍长
自动转换(当前顶层便捷函数)简洁直观、学习成本低在高频路径里有额外转换开销

最终选择:两者都支持,让用户自由选择!

// ✅ 简单场景:直接用 DSL 对象
validate({ email: 'email!' }, data);

// ✅ 复杂场景:显式转换
const schema = dsl({ email: 'email!' });
validate(schema, data);

对应示例文件

示例入口: validate-dsl-object-support.ts
说明: 覆盖直接传入 DSL 对象、混合使用 DslBuilder 与 DSL 字符串,以及顶层 validate() / validateAsync() 的真实支持边界。