⚡ TL;DR (30-second intro)
What is schema-dsl?
Write field rules like this:
import { dsl, validate } from 'schema-dsl';
const userSchema = dsl({
username: 'string:3-32!',
email: 'email!',
role: 'admin|user|guest',
contact: 'types:email|phone'
});
const result = validate(userSchema, req.body);
Then that same set of rules continues to power:
- ✅ Sync / async validation —
validate() / validateAsync()
- ✅ Schema derivation —
pick / omit / partial to tailor schemas per endpoint
- ✅ Database schemas — export directly to MongoDB / MySQL / PostgreSQL
- ✅ Field documentation — auto-generate Markdown
- ✅ Unified error model —
ValidationError + I18nError
- ✅ Internationalization — 5 built-in locales (zh-CN / en-US / ja-JP / es-ES / fr-FR), switchable at runtime
5-minute tutorial: Quick Start | Full docs: Online Documentation
🗺️ Documentation
Getting started:
Core features:
Export & integration:
Full docs: Online Documentation · Chinese Documentation · Feature Index
✨ Why schema-dsl?
🎯 Minimal DSL — 65% less code
|
❌ Traditional approach — verbose
const schema = Joi.object({
username: Joi.string()
.min(3).max(32).required(),
email: Joi.string()
.email().required(),
age: Joi.number()
.min(18).max(120)
});
|
✅ schema-dsl — concise and clean
const schema = dsl({
username: 'string:3-32!',
email: 'email!',
age: 'number:18-120'
});
|
💪 Full-featured
| Basic validation | ✅ | string, number, boolean, date, email, url, phone… |
| Advanced validation | ✅ | regex, custom functions, conditional branches, nested objects, arrays… |
| Cross-type union | ✅ | types:email|phone — one field accepts multiple types |
| Error messages | ✅ | auto-translated + custom messages + field labels |
| i18n business errors | ✅ | I18nError with numeric error codes |
| Database export | ✅ | MongoDB / MySQL / PostgreSQL schema generation |
| Documentation generation | ✅ | Markdown field docs auto-generated |
| TypeScript | ✅ | Written in native TypeScript with full type inference |
| Plugin system | ✅ | Custom types / formats / validators |
| Schema reuse | ✅ | pick / omit / partial / extend |
🎨 One schema, many uses (unique capability)
import { dsl, exporters, SchemaUtils } from 'schema-dsl';
const userSchema = dsl({
id: 'uuid!',
username: 'string:3-32!',
email: 'email!',
password: 'string:8-64!',
age: 'number:18-120',
createdAt: 'string!'
});
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
const updateSchema = SchemaUtils.partial(SchemaUtils.pick(userSchema, ['username', 'email']));
const publicSchema = SchemaUtils.omit(userSchema, ['password']);
const mongoSchema = new exporters.MongoDBExporter().export(userSchema);
const mysqlDDL = new exporters.MySQLExporter().export('users', userSchema);
const pgDDL = new exporters.PostgreSQLExporter().export('users', userSchema);
const markdown = exporters.MarkdownExporter.export(userSchema, { title: 'User Field Reference' });
⚠️ SQL exporters only accept anyOf / oneOf when every branch resolves to the same SQL column type (for example ipv4 | ipv6). Ambiguous unions such as string | number now throw an explicit error instead of silently choosing the first branch.
📦 Installation
npm install schema-dsl
Runtime requirement: Node.js >= 18.0.0
🚀 Quick Start
1. Basic validation
import { dsl, validate } from 'schema-dsl';
const userSchema = dsl({
username: 'string:3-32!',
email: 'email!',
age: 'number:18-120',
role: 'admin|user|guest',
tags: 'array<string>'
});
const result = validate(userSchema, {
username: 'john_doe',
email: 'john@example.com',
age: 25,
role: 'user',
tags: ['verified']
});
console.log(result.valid);
console.log(result.data);
const bad = validate(userSchema, { username: 'ab', email: 'not-email' });
console.log(bad.errors);
2. Async validation + Express integration
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
const createUserSchema = dsl({
username: 'string:3-32!',
email: 'email!',
password: 'string:8-32!'
});
app.post('/api/users', async (req, res, next) => {
try {
const validData = await validateAsync(createUserSchema, req.body);
const user = await db.users.create(validData);
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
});
app.use((error, req, res, next) => {
if (error instanceof ValidationError) {
return res.status(400).json({ success: false, errors: error.errors });
}
next(error);
});
3. Schema reuse (create / update / public)
import { dsl, SchemaUtils } from 'schema-dsl';
const userSchema = dsl({
id: 'uuid!',
username: 'string:3-32!',
email: 'email!',
password: 'string:8-64!',
createdAt: 'string!'
});
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
const updateSchema = SchemaUtils.partial(
SchemaUtils.pick(userSchema, ['username', 'email'])
);
const publicSchema = SchemaUtils.omit(userSchema, ['password']);
4. Database schema export
import { dsl, exporters } from 'schema-dsl';
const productSchema = dsl({
name: 'string:1-100!',
price: 'number:>0!',
stock: 'integer:0-!',
category: 'string!',
createdAt: 'datetime!'
});
const mongoSchema = new exporters.MongoDBExporter().export(productSchema);
const mysqlDDL = new exporters.MySQLExporter().export('products', productSchema);
const markdown = exporters.MarkdownExporter.export(productSchema, { title: 'Product Field Reference' });
🗒️ Feature Overview
Common use cases
📖 DSL Syntax Reference
Basic types
dsl({
name: 'string!',
code: 'string:6',
bio: 'string:-500',
username: 'string:3-32',
age: 'number:18-120',
score: 'integer:0-100',
price: 'number:>0',
level: 'number:>=1',
status: 'active|inactive|pending',
tier: 'enum:number:1|2|3',
tags: 'array<string>',
items: 'array:1-10<number>',
active: 'boolean!',
contact: 'types:email|phone!',
price2: 'types:number:0-|string',
})
Built-in formats
dsl({
email: 'email!',
website: 'url!',
birthday: 'date!',
createdAt: 'datetime!',
userId: 'uuid!',
phone: 'phone:cn!',
idCard: 'idCard:cn!',
slug: 'slug:3-100!',
})
Fluent chain API (recommended for TypeScript)
import { dsl } from 'schema-dsl';
const schema = dsl({
username: dsl('string:3-32!')
.username()
.label('username')
.messages({ required: 'Username is required' }),
email: dsl('email!').label('email address'),
phone: dsl('string:11!')
.pattern(/^1[3-9]\d{9}$/)
.label('phone number'),
});
Conditional validation
const contactSchema = dsl({
type: 'email|phone|wechat',
contact: dsl.match('type', {
email: 'email!',
phone: 'string:11!',
wechat: 'string:6-20!',
})
});
const orderSchema = dsl({
isVip: 'boolean!',
discount: dsl.if('isVip', 'number:10-50!', 'number:0-10')
});
dsl.if(d => !d.account)
.message('Account not found')
.and(d => d.account.balance < amount)
.message('Insufficient balance')
.assert(data);
🌍 Internationalization
import { dsl, validate, Locale, I18nError } from 'schema-dsl';
const result = validate(schema, data, { locale: 'en-US' });
Locale.addLocale('zh-CN', {
'user.notFound': 'User not found',
'user.forbidden': { code: 40003, message: 'Access forbidden' },
});
I18nError.assert(user, 'user.notFound');
I18nError.throw('user.forbidden', {}, 403);
I18nError.assert(ok, 'user.notFound', {}, 404, locale);
try {
await api.getUser(id);
} catch (error) {
switch (error.code) {
case 40003: showForbiddenPage(); break;
}
}
🔌 Plugin System
import { PluginManager, Validator, dsl } from 'schema-dsl';
const pluginManager = new PluginManager();
pluginManager.register({
name: 'extra-formats',
install(core) {
const validator = core as Validator;
validator.addFormat('hex-color', {
validate: (v: string) => /^#[0-9A-F]{6}$/i.test(v)
});
validator.addFormat('mac-address', {
validate: (v: string) => /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/i.test(v)
});
}
});
const validator = new Validator();
pluginManager.install(validator);
const schema = dsl({ color: 'hex-color!', mac: 'mac-address' });
const result = validator.validate(schema, { color: '#FF5733', mac: '00:1A:2B:3C:4D:5E' });
🔧 Core API Reference
dsl(schema) | Create a schema | Schema object | DSL Syntax |
validate(schema, data) | Synchronous validation | { valid, errors, data } | validate() |
validateAsync(schema, data) | Asynchronous validation | Promise (throws on failure) | Async Validation |
SchemaUtils.pick() | Select fields | New schema | SchemaUtils |
SchemaUtils.omit() | Exclude fields | New schema | SchemaUtils |
SchemaUtils.partial() | Make all fields optional | New schema | SchemaUtils |
dsl.if(condition) | Conditional validation | ConditionalBuilder | Conditional API |
dsl.match(field, map) | Branch validation | ConditionalBuilder | Conditional API |
I18nError.throw() | Throw an i18n error | never | Error Handling |
I18nError.assert() | Assert then throw | void | Error Handling |
📝 TypeScript Usage
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
const userSchema = dsl({
username: dsl('string:3-32!').label('username'),
email: dsl('email!').label('email'),
age: dsl('number:18-100').label('age')
});
try {
const validData = await validateAsync(userSchema, payload);
} catch (error) {
if (error instanceof ValidationError) {
error.errors.forEach(e => console.log(`${e.path}: ${e.message}`));
}
}
Note: In TypeScript projects, wrap strings with dsl('...') to get type inference. In JavaScript projects you can pass strings directly.
See the TypeScript Guide for details.
🛠️ Development
npm run build
npm run test
npm run typecheck
Local documentation preview:
cd website
npm run dev
🤝 Contributing
git clone https://github.com/vextjs/schema-dsl.git
cd schema-dsl
npm install
npm test
See CONTRIBUTING.md for details.
🔗 Links
📖 Core documentation
🎯 Feature documentation
🗄️ Export & integration
💻 Examples
📝 Changelog & contributing
📄 License
Apache-2.0
If this project is useful to you, please consider giving it a Star ⭐
Made with ❤️ by the schema-dsl team