Declare field rules with the simplest DSL β let one schema drive validation, derivation, export, and documentation.
π Documentation: https://vextjs.github.io/schema-dsl
Quick Start Β· Documentation Β· Feature Overview Β· Examples
npm install schema-dslWhat 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 / 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:
- validate() β synchronous validation API
- SchemaUtils β schema reuse
- Conditional Validation API β s.if / s.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
- Extensions and Integration β custom types, factories, chain methods, keywords, and plugins
Full docs: Online Documentation Β· Chinese Documentation Β· Feature Index
|
β 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 // just 3 lines
const schema = s({
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 |
| Extension system | β | Custom DSL types / factories / chain methods / formats / 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 |
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!'
});
// π 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
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.
| Entry | Purpose |
|---|---|
schema-dsl/pure |
Recommended default entry from v2.1.0; 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. |
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.
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>'
});
// β
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 { 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 {
// 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 { s, SchemaUtils } from 'schema-dsl/pure';
const userSchema = s({
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 { s, exporters } from 'schema-dsl/pure';
const productSchema = s({
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` DOUBLE NOT NULL,
`stock` BIGINT NOT NULL,
`category` VARCHAR(255) NOT NULL,
`createdAt` DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
*/
// 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() |
validate() |
| 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 | s.if() / s.match() |
Conditional API |
| Custom DSL types | s.registerExtension() / DslBuilder.registerType() |
Extensions Overview |
| No global String extension | schema-dsl/pure |
API Reference |
| Compile-time string-chain transform | transformSchemaDsl() / schemaDslEsbuildPlugin() |
API Reference |
s({
// 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
})s({
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 { 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(),
});// s.match β route to different rules based on a field value
const contactSchema = s({
type: 'email|phone|wechat',
contact: s.match('type', {
email: 'email!',
phone: 'string:11!',
wechat: 'string:6-20!',
})
});
// s.if β simple conditional branch
const orderSchema = s({
isVip: 'boolean!',
discount: s.if('isVip', 'number:10-50!', 'number:0-10')
});
// s.if chain assertion
s.if(d => !d.account)
.message('Account not found')
.and(d => d.account.balance < amount)
.message('Insufficient balance')
.assert(data);import { s, validate, Locale, I18nError } from 'schema-dsl/pure';
// 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;
}
}Use direct extension APIs for simple custom DSL types, factories, and chain methods. Use PluginManager when you want to package several extension hooks together.
import { PluginManager, Validator, s } from 'schema-dsl/pure';
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 = s({ color: 'hex-color!', mac: 'mac-address' });
const result = validator.validate(schema, { color: '#FF5733', mac: '00:1A:2B:3C:4D:5E' });| API | Purpose | Returns | Docs |
|---|---|---|---|
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 | 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 |
import { s, validateAsync, ValidationError } from 'schema-dsl/pure';
// β
wrap strings with s() in TypeScript for builder method hints
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);
// validData's static type is controlled by the generic passed to validateAsync<T>.
// Use InferSchema / InferDslDefinition for schema-literal value type extraction.
} catch (error) {
if (error instanceof ValidationError) {
error.errors.forEach(e => console.log(`${e.path}: ${e.message}`));
}
}Note: In TypeScript projects, use
s('...')ors.xxx()to get builder chain hints without adding globalStringdeclarations. DSL string literals also support lightweight value-type extraction throughInferSchema/InferDslString, but constraints such as length ranges, regexes, defaults, and custom validators remain runtime schema rules. 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
- validate() β synchronous validation API
- 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
- Run all documentation examples with
npm run examples:run. - 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
- object-dsl-builder.ts β object builder chaining and required-field control
- real-world.ts β combined production-style schemas for users, products, orders, and queries
- 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