Declare field rules with the simplest DSL β let one schema drive validation, derivation, export, and documentation.
Quick Start Β· Documentation Β· Feature Overview Β· Examples
npm install schema-dslWhat 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 / partialto 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
Getting started:
- Quick Start β up and running in 5 minutes
- DSL Syntax Reference β syntax cheatsheet
- FAQ β common questions
Core features:
- Validation Guide β all validation scenarios
- SchemaUtils β schema reuse
- Conditional Validation API β dsl.if / dsl.match
- Async Validation & Framework Integration β Express / Koa / Fastify
- Error Handling & i18n β error model
Export & integration:
- Export Guide β MongoDB / MySQL / PostgreSQL
- TypeScript Guide β type inference and usage
- Plugin System β custom extensions
Full docs: Online Documentation Β· Feature Index
|
β Traditional approach β verbose // Joi β requires 8 lines
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 // just 3 lines
const schema = dsl({
username: 'string:3-32!',
email: 'email!',
age: 'number:18-120'
}); |
| Feature | schema-dsl | Notes |
|---|---|---|
| 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 |
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!'
});
// π derive scenario-specific schemas
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
const updateSchema = SchemaUtils.partial(SchemaUtils.pick(userSchema, ['username', 'email']));
const publicSchema = SchemaUtils.omit(userSchema, ['password']);
// ποΈ export the same schema to any database
const mongoSchema = new exporters.MongoDBExporter().export(userSchema);
const mysqlDDL = new exporters.MySQLExporter().export('users', userSchema);
const pgDDL = new exporters.PostgreSQLExporter().export('users', userSchema);
// π generate field documentation from the same schema
const markdown = exporters.MarkdownExporter.export(userSchema, { title: 'User Field Reference' });
β οΈ SQL exporters only acceptanyOf/oneOfwhen every branch resolves to the same SQL column type (for exampleipv4 | ipv6). Ambiguous unions such asstring | numbernow throw an explicit error instead of silently choosing the first branch.
npm install schema-dslRuntime requirement: Node.js >= 18.0.0
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>'
});
// β
validation passed
const result = validate(userSchema, {
username: 'john_doe',
email: 'john@example.com',
age: 25,
role: 'user',
tags: ['verified']
});
console.log(result.valid); // true
console.log(result.data); // validated data
// β validation failed
const bad = validate(userSchema, { username: 'ab', email: 'not-email' });
console.log(bad.errors);
// [
// { path: 'username', message: 'username must be at least 3 characters' },
// { path: 'email', message: 'email must be a valid email address' }
// ]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 {
// throws ValidationError automatically on failure
const validData = await validateAsync(createUserSchema, req.body);
const user = await db.users.create(validData);
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
});
// global error handler
app.use((error, req, res, next) => {
if (error instanceof ValidationError) {
return res.status(400).json({ success: false, errors: error.errors });
}
next(error);
});import { dsl, SchemaUtils } from 'schema-dsl';
const userSchema = dsl({
id: 'uuid!',
username: 'string:3-32!',
email: 'email!',
password: 'string:8-64!',
createdAt: 'string!'
});
// create endpoint: remove server-generated fields
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
// update endpoint: pick editable fields, all optional
const updateSchema = SchemaUtils.partial(
SchemaUtils.pick(userSchema, ['username', 'email'])
);
// public response: hide sensitive fields
const publicSchema = SchemaUtils.omit(userSchema, ['password']);import { dsl, exporters } from 'schema-dsl';
const productSchema = dsl({
name: 'string:1-100!',
price: 'number:>0!',
stock: 'integer:0-!',
category: 'string!',
createdAt: 'datetime!'
});
// MongoDB $jsonSchema (for db.createCollection() document validation; not a Mongoose model schema)
const mongoSchema = new exporters.MongoDBExporter().export(productSchema);
/*
{
$jsonSchema: {
bsonType: 'object',
properties: {
name: { bsonType: 'string', minLength: 1, maxLength: 100 },
price: { bsonType: 'double', minimum: 0 },
stock: { bsonType: 'int', minimum: 0 },
category: { bsonType: 'string' },
createdAt: { bsonType: 'string' }
},
required: ['name', 'price', 'stock', 'category', 'createdAt']
}
}
*/
// MySQL DDL
const mysqlDDL = new exporters.MySQLExporter().export('products', productSchema);
/*
CREATE TABLE `products` (
`name` VARCHAR(100) NOT NULL,
`price` DECIMAL(10, 2) NOT NULL,
`stock` INT NOT NULL,
`category` VARCHAR(255) NOT NULL,
`createdAt` DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
// Markdown field documentation
const markdown = exporters.MarkdownExporter.export(productSchema, { title: 'Product Field Reference' });| Use case | API | Docs |
|---|---|---|
| API parameter validation | validateAsync + ValidationError |
Async Validation |
| Form / script validation | validate() |
Validation Guide |
| Batch data validation | SchemaUtils.validateBatch() |
SchemaUtils |
| create / update derivation | pick / omit / partial |
SchemaUtils |
| Database table creation | MongoDBExporter / MySQLExporter |
Export Guide |
| Field documentation | MarkdownExporter |
Export Guide |
| Multilingual API errors | I18nError |
Error Handling |
| Conditional / dynamic rules | dsl.if() / dsl.match() |
Conditional API |
| Custom type extensions | PluginManager |
Plugin System |
dsl({
// string
name: 'string!', // required
code: 'string:6', // exact length 6
bio: 'string:-500', // max length 500
username: 'string:3-32', // length range 3β32
// number
age: 'number:18-120', // range 18β120
score: 'integer:0-100', // integer 0β100
price: 'number:>0', // strictly greater than 0
level: 'number:>=1', // greater than or equal to 1
// enum
status: 'active|inactive|pending', // string enum
tier: 'enum:number:1|2|3', // numeric enum
// array
tags: 'array<string>', // string array
items: 'array:1-10<number>', // 1β10 numeric elements
// boolean
active: 'boolean!',
// union type
contact: 'types:email|phone!', // email or phone, required
price2: 'types:number:0-|string', // number or string
})dsl({
email: 'email!', // email address
website: 'url!', // URL
birthday: 'date!', // YYYY-MM-DD
createdAt: 'datetime!', // ISO 8601
userId: 'uuid!', // UUID
phone: 'phone:cn!', // Chinese mobile number
idCard: 'idCard:cn!', // Chinese national ID
slug: 'slug:3-100!', // URL-friendly string
})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'),
});// dsl.match β route to different rules based on a field value
const contactSchema = dsl({
type: 'email|phone|wechat',
contact: dsl.match('type', {
email: 'email!',
phone: 'string:11!',
wechat: 'string:6-20!',
})
});
// dsl.if β simple conditional branch
const orderSchema = dsl({
isVip: 'boolean!',
discount: dsl.if('isVip', 'number:10-50!', 'number:0-10')
});
// dsl.if chain assertion
dsl.if(d => !d.account)
.message('Account not found')
.and(d => d.account.balance < amount)
.message('Insufficient balance')
.assert(data);import { dsl, validate, Locale, I18nError } from 'schema-dsl';
// built-in locales: zh-CN / en-US / ja-JP / es-ES / fr-FR (auto-loaded, no configuration needed)
const result = validate(schema, data, { locale: 'en-US' });
// error messages automatically use the specified locale
// register a custom locale
Locale.addLocale('zh-CN', {
'user.notFound': 'User not found',
'user.forbidden': { code: 40003, message: 'Access forbidden' },
});
// throw i18n business errors
I18nError.assert(user, 'user.notFound'); // auto-throw when user is falsy
I18nError.throw('user.forbidden', {}, 403); // throw directly
I18nError.assert(ok, 'user.notFound', {}, 404, locale); // specify locale at runtime
// errors carry a numeric code; frontend can branch on it
try {
await api.getUser(id);
} catch (error) {
switch (error.code) {
case 40003: showForbiddenPage(); break;
}
}import { PluginManager, Validator, dsl } from 'schema-dsl';
const pluginManager = new PluginManager();
// register a custom format plugin (must provide an install function)
pluginManager.register({
name: 'extra-formats',
install(core) {
const validator = core as Validator;
// register custom formats on the Validator instance via addFormat
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)
});
}
});
// create a Validator and install plugins
const validator = new Validator();
pluginManager.install(validator);
// use the custom formats in a schema
const schema = dsl({ color: 'hex-color!', mac: 'mac-address' });
const result = validator.validate(schema, { color: '#FF5733', mac: '00:1A:2B:3C:4D:5E' });| API | Purpose | Returns | Docs |
|---|---|---|---|
dsl(schema) |
Create a schema | Schema object | DSL Syntax |
validate(schema, data) |
Synchronous validation | { valid, errors, data } |
Validation Guide |
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 |
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
// β
wrap strings with dsl() in TypeScript for full type inference
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);
// validData has full type inference
} 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.
npm run build # compile TypeScript
npm run test # run tests
npm run typecheck # type checkLocal documentation preview:
cd website
npm run devgit clone https://github.com/vextjs/schema-dsl.git
cd schema-dsl
npm install
npm testSee CONTRIBUTING.md for details.
- Quick Start β up and running in 5 minutes
- DSL Syntax Guide β complete syntax reference
- Validation Guide β advanced validation techniques
- API Reference β complete API docs
- TypeScript Guide β required reading for TS users
- Best Practices β avoid common pitfalls
- Troubleshooting β diagnosing issues
- SchemaUtils
- Conditional Validation API
- Async Validation
- Error Handling & i18n
- Union Types
- Enum Types
- Export Guide
- MongoDB Exporter
- MySQL Exporter
- PostgreSQL Exporter
- Markdown Exporter
β οΈ Export Limitations
- quick-start.ts β basic usage and registration form
- validate-async.ts β async validation and
ValidationErrorhandling - export-guide.ts β database export overview
- error-handling.ts β field errors and business error handling
- plugin-system.ts β plugin system and hooks
If this project is useful to you, please consider giving it a Star β
Made with β€οΈ by the schema-dsl team