β‘ TL;DR (30-second intro)
What is schema-dsl?
Write field rules like this:
import { s, validate } from 'schema-dsl/pure';
const userSchema = s({
username: s('string:3-32!').label('Username'),
email: s('email!').label('Email'),
role: 'admin|user|guest',
contact: 'types:email|phone'
});
const contactEmail = s('email!').label('Email').pattern(/custom/);
const accountEmail = s.email().label('Email').pattern(/custom/).require();
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?
π― Concise field rules β less boilerplate
|
β Manual JSON Schema β verbose
const schema = {
type: 'object',
properties: {
username: { type: 'string', minLength: 3, maxLength: 32 },
email: { type: 'string', format: 'email' },
age: { type: 'number', minimum: 18, maximum: 120 }
},
required: ['username', 'email']
};
|
β
schema-dsl β concise and clean
const schema = s({
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 |
| Extension system | β
| One business type definition for pure DSL, s('...'), and s.xxx() entries; plus formats and validators |
| Schema reuse | β
| pick / omit / partial / extend |
| Side-effect-controlled entries | β
| root compatibility, schema-dsl/pure for no String.prototype installation, and schema-dsl/runtime for isolated runtime state |
| Compile-time transform | β
| schema-dsl/transform core and optional schema-dsl/esbuild adapter |
Progressive s authoring | β
| Use plain DSL strings, s('email!'), or s.email(); all converge to the same builder implementation |
π¨ One schema, many uses (unique capability)
import { s, exporters, SchemaUtils } from 'schema-dsl/pure';
const userSchema = s({
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
The entry points below are part of the package contract and are covered by packed-consumer tests for both ESM and CommonJS.
π¦ Package Entry Points
schema-dsl keeps the root import compatible with v1-style direct string chaining, and also exposes explicit entries for projects that want tighter control over global side effects.
schema-dsl/pure | Recommended default entry for application code; exports the s / dsl namespace and validation helpers without installing String.prototype extensions. |
schema-dsl/runtime | Runtime adapter factory for per-tenant/per-app isolated Locale messages, messageProvider, TypeRegistry scope, PATTERNS, validator instances and I18nError creation. |
schema-dsl | Root compatibility entry; imports install the non-enumerable String chain API by default. Prefer schema-dsl/pure for new public examples. |
schema-dsl/compat | Explicit compatibility entry that installs String extensions on import. |
schema-dsl/register-string | Side-effect entry for explicitly registering String extensions during application startup. |
schema-dsl/string-types | Opt-in TypeScript declarations for String-chain authoring; no runtime prototype installation. |
schema-dsl/transform | Babel AST transform core that rewrites static string-chain calls into helper calls imported from schema-dsl/pure. Babel AST packages are optional peer dependencies for projects that use this entry. |
schema-dsl/esbuild | Optional esbuild plugin adapter around the transform core. esbuild is an optional peer dependency. |
import { s, validate } from 'schema-dsl/pure';
import { transformSchemaDsl } from 'schema-dsl/transform';
import { schemaDslEsbuildPlugin } from 'schema-dsl/esbuild';
import { createRuntime } from 'schema-dsl/runtime';
const schema = s({
email: 'email!',
username: s('string:3-32!').label('Username'),
backupEmail: s.email().label('Backup email').require()
});
const transformed = transformSchemaDsl(
'export const field = "admin|user|guest".label("Role")',
{ filename: 'schema.ts' }
);
const plugins = [schemaDslEsbuildPlugin()];
const tenantRuntime = createRuntime({
locale: 'tenant-a',
messages: {
'tenant.user.missing': { code: 'TENANT_USER_MISSING', message: 'Tenant user {{#id}} is missing' }
},
types: {
tenantId: { type: 'string', pattern: '^tenant_[a-z0-9]+$' }
},
messageProvider: ({ key, locale, fallback }) =>
key === 'number.min' ? `[${locale}] {{#label}} must be >= {{#limit}}` : fallback
});
const tenantSchema = tenantRuntime.s({
id: 'tenantId!',
age: 'number:18-120'
});
const tenantEmail = tenantRuntime.s.email().label('Tenant email').require().toSchema();
const tenantResult = tenantRuntime.validate(tenantSchema, { id: 'tenant_demo', age: 16 });
The transform handles static DSL string literals, including naked pipe enums such as "admin|user|guest", and injects imports from schema-dsl/pure. By default it rewrites the complete built-in String-chain API (.label(), .pattern(), .require(), .required(), .toJsonSchema(), and the other methods installed by schema-dsl). Use additionalMethods for user-defined chain methods, and additionalTypes / additionalTypePatterns for registered custom DSL type literals such as "tenant-id!".label("Tenant"); methods remains a legacy replacement set when you intentionally want to override the built-in default list. Dynamic expressions, computed member calls, and already transformed helper calls are left unchanged.
Use schema-dsl/pure for ordinary application code. Use schema-dsl/runtime when a framework needs independent runtime state per app, tenant, worker, or plugin host. createRuntime() keeps message lookup, per-call messageProvider, runtime custom types, namespace factories, pattern overrides, validator caches, custom keyword messages, conditional branches, async custom validators, and createI18nError() inside that runtime instance. Use one runtime for the app/plugin lifecycle, pass request-level locale, messages, messageProvider or { coerce: false } via per-call options, and call configure(..., { mode: 'replace' | 'reset' }), clearCache(), getStats() or dispose() for hot reload and shutdown.
createSchemaDslRuntime() and createSchemaDslAdapter() are equivalent aliases of createRuntime() for adapter-oriented integrations.
π Quick Start
1. Basic validation
import { s, validate } from 'schema-dsl/pure';
const userSchema = s({
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 { s, validateAsync, ValidationError } from 'schema-dsl/pure';
const createUserSchema = s({
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 { s, SchemaUtils } from 'schema-dsl/pure';
const userSchema = s({
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 { s, exporters } from 'schema-dsl/pure';
const productSchema = s({
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
s({
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
s({
email: 'email!',
website: 'url!',
birthday: 'date!',
createdAt: 'datetime!',
userId: 'uuid!',
phone: 'phone:cn!',
idCard: 'idCard:cn!',
slug: 'slug:3-100!',
})
Fluent chain API (s progressive entries)
import { s } from 'schema-dsl/pure';
const schema = s({
username: s('string:3-32!')
.username()
.label('username')
.messages({ required: 'Username is required' }),
email: s('email!').label('email address'),
phone: s('string:11!')
.pattern(/^1[3-9]\d{9}$/)
.label('phone number'),
recoveryEmail: s.email()
.label('recovery email')
.pattern(/@company\.com$/)
.require(),
});
Conditional validation
const contactSchema = s({
type: 'email|phone|wechat',
contact: s.match('type', {
email: 'email!',
phone: 'string:11!',
wechat: 'string:6-20!',
})
});
const orderSchema = s({
isVip: 'boolean!',
discount: s.if('isVip', 'number:10-50!', 'number:0-10')
});
s.if(d => !d.account)
.message('Account not found')
.and(d => d.account.balance < amount)
.message('Insufficient balance')
.assert(data);
π Internationalization
import { s, validate, Locale, I18nError } from 'schema-dsl/pure';
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;
}
}
π Extensions and Plugin Packaging
Use custom DSL types when one business type should work through pure DSL strings, s('...'), and s.xxx() factories. Use PluginManager when you want to package validator formats, keywords, lifecycle hooks, or several extension hooks together.
import { PluginManager, Validator, s } from 'schema-dsl/pure';
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 = s({ color: 'hex-color!', mac: 'mac-address' });
const result = validator.validate(schema, { color: '#FF5733', mac: '00:1A:2B:3C:4D:5E' });
π§ Core API Reference
s(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, or only selected fields optional when fields is provided | New schema | SchemaUtils |
s.if(condition) | Conditional validation | ConditionalBuilder | Conditional API |
s.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 |
schema-dsl/pure | Import the API without installing String extensions | API namespace | API Reference |
schema-dsl/string-types | Opt into TypeScript hints for String-chain authoring | Type declarations | TypeScript Usage |
transformSchemaDsl() | Rewrite static string-chain DSL calls at compile time | { code, changed, warnings } | API Reference |
schemaDslEsbuildPlugin() | Use the transform in esbuild build/context flows | esbuild plugin | API Reference |
π TypeScript Usage
import { s, validateAsync, ValidationError } from 'schema-dsl/pure';
const userSchema = s({
username: s('string:3-32!').label('username'),
email: s('email!').label('email'),
age: s('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, use s('...') or s.xxx() to get builder chain hints without adding global String declarations. DSL string literals also support lightweight value-type extraction through InferSchema / InferDslString, but constraints such as length ranges, regexes, defaults, and custom validators remain runtime schema rules.
See the TypeScript Guide for details.
π οΈ Development
npm run build
npm run test
npm run typecheck
npm run docs:linkcheck
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