A complete guide to DSL syntax

This page is the main reference for writing schema rules. Read it after Quick Start, then use Type Reference and Validation Guide for deeper behavior.

quick start

import { s } from 'schema-dsl/pure';

//Basic type
const schema = s({
  name: 'string!', // required string
  age: 'number', // optional number
  email: 'email!', // required email
  active: 'boolean', // Boolean value
  tags: 'array<string>' // String array
});

// Factory entry for stronger method discovery:
const emailField = s.email().label('Email').require();

Use the same schema to validate real data:

import { s, validate } from 'schema-dsl/pure';

const userSchema = s({
  name: 'string:1-50!',
  email: 'email!',
  age: 'number:18-120',
  role: 'admin|user|guest'
});

validate(userSchema, {
  name: 'John',
  email: 'john@example.com',
  age: 28,
  role: 'user'
}).valid; // true

validate(userSchema, {
  name: '',
  email: 'not-email',
  age: 15,
  role: 'owner'
}).valid; // false

Complete list of types

basic type

typeDSLDescription
stringstringtext type
numbernumberfloating point number
integerintegerinteger
Booleanbooleantrue/false
objectobjectNested objects
arrayarrayarray type
null valuenullnull value
arbitraryanyany type

format type

typeDSLDescription
MailemailEmail address
URLurlURL
URIuriURI address
UUIDuuidUUID format
datedateYYYY-MM-DD
date timedatetimeISO 8601
timetimeHH:mm:ss
hostnamehostnamehostname
IP(IPv4 / IPv6)ipAutomatically accept IPv4 or IPv6
IPv4ipv4IPv4 address
IPv6ipv6IPv6 address
binarybinaryBase64 encoding

special type

typeDSLDescription
ObjectIdobjectIdMongoDB ObjectId
Hexadecimal colorhexColorCSS hex color
MAC addressmacAddressMAC address
Cron expressioncronStandard cron expressions
URL SlugslugURL-friendly logo composed of lowercase letters/numbers/dash lines
Chinese namechineseName2 to 10 Chinese characters
Pure Chinese textchineseOnly Chinese characters allowed
Email domain name validationemailDomainDomain name constraint type based on email format
AlphanumericalphanumLetters and numbers only
All lowercase stringlowerAutomatically constrain to lowercase strings
all caps stringupperAutomatically constrain to uppercase strings
JSON stringjsonThe content must be a legal JSON string
port numberportinteger port number

basic grammar

1. Type definition

//Basic type
'string' // string
'number' // number
'integer' // integer
'boolean' // boolean

// format type
'email' // Email
'url'         // URL
'date' // date
'uuid'        // UUID

2. Required and optional marks

Use ! to mark required fields and ? to explicitly mark optional fields:

const schema = s({
  username: 'string!', // required
  nickname: 'string', // optional (default)
  bio: 'string?', // Explicitly optional (equivalent to string)
  email: 'email?' // Optional email
});

Description:

  • ! - ​​required tag, field must exist
  • ? - ​​optional tag, field can be omitted (explicit expression)
  • Unmarked - optional by default (field can be omitted)

recommend:

  • Clearly mark required fields using !
  • Use ? to enhance code readability when you need to explicitly express "optional"

3. Object required

Two methods are supported:

// Method 1: Required inside the field
const schema1 = s({
  user: {
    name: 'string!', // name is required (user is optional)
    email: 'email!' // email is required
  }
});

// Method 2: The object itself is required ✅ Recommended
const schema2 = s({
  'user!': { // user itself is required
    name: 'string', // name optional
    email: 'email' // email optional
  }
});

constraint syntax

1. String length

'string:10' // Exact length 10 (exactLength: minLength=10, maxLength=10)
'string:-10' // Maximum length 10
'string:3-32' // length range 3-32
'string:10-' // Minimum length 10 (no maximum limit)

Example:

const schema = s({
  username: 'string:3-32!', // 3-32 characters, required
  bio: 'string:500', // Maximum 500 characters
  password: 'string:8-' // minimum 8 characters
});

2. Numeric range

'number:100' // Maximum value 100
'number:0-100' // Range 0-100
'number:18-' // minimum value 18

Example:

const schema = s({
  age: 'number:18-120',         // 18-120
  score: 'number:100',          // 0-100
  price: 'number:0-'            // ≥0
});

3. Enumeration values

Use | to separate enumeration values:

const schema = s({
  status: 'active|inactive|pending',
  gender: 'male|female|other!',
  role: 'admin|user|guest'
});

4. types: Union type

When a field needs to accept multiple different types, you can use the types: prefix to generate a union type:

const schema = s({
  contact: 'types:email|phone',
  price: 'types:number:0-|string:1-20',
  payload: 'types:object|array<object>'
});

This syntax will be compiled into the oneOf structure, which is suitable for scenarios where "any one of the types is satisfied".

Applicable scenarios:

  • Contact information allows email or mobile phone number
  • Price fields allow numeric values ​​or description strings
  • Compatible with multiple input formats for the same field in the history interface

5. Special constraints

Support format-specific constraints:

'phone:cn' // China mobile phone number
'idCard:cn' // Chinese ID card
'creditCard:visa' // Visa credit card
'licensePlate:cn' // Chinese license plate
'postalCode:cn' // China postal code
'passport:cn' // Chinese passport

Example:

const schema = s({
  mobile: 'phone:cn!',
  id: 'idCard:cn',
  card: 'creditCard:mastercard'
});

Array syntax

1. Basic array

'array' // Array of any type
'array<string>' // string array
'array<number>' // array of numbers
'array<integer>' // integer array

2. Array length constraints

'array:1-10' // 1-10 elements
'array!1-10' // 1-10 elements, required
'array:1-' // at least 1 element
'array:-10' // up to 10 elements
'array:1-10<string>' // 1-10 string elements

Example:

const schema = s({
  tags: 'array!1-10<string>', // required, 1-10 strings
  scores: 'array:1-5<number>', // optional, 1-5 numbers
  items: 'array:1-<string>' // at least 1 string
});

3. Array element constraints

const schema = s({
  tags: 'array<string:1-20>', // 1-20 characters per string
  scores: 'array<number:0-100>', // each number 0-100
  ids: 'array<integer:1->' // Each integer ≥ 1
});

4. Nested arrays

// two-dimensional array
const schema = s({
  matrix: 'array<array<number>>'
});

// array of objects
const schema = s({
  users: 'array<object>',
  // detailed object item definition
  items: s.array({
    name: 'string!',
    age: 'number'
  })
});

object syntax

1. Basic objects

const schema = s({
  user: {
    name: 'string!',
    email: 'email!',
    age: 'number'
  }
});

2. Nested objects

const schema = s({
  user: {
    profile: {
      bio: 'string:500',
      social: {
        twitter: 'url',
        github: 'url'
      }
    }
  }
});

3. Mixed nesting

const schema = s({
  'user!': { // user required
    name: 'string!', // name is required
    contacts: 'array!1-5<object>', // 1-5 contact information
    tags: 'array<string:1-20>' // String array
  }
});

Conditional validation (Match)

Supports more elegant conditional validation syntax s.match and s.if.

Similar to switch-case, the validation rules for the current field are determined based on the value of a certain field.

grammar:

s.match(field, {
  value1: 'schema1',
  value2: 'schema2',
  _default: 'defaultSchema' // optional
})

Example:

const schema = s({
  contactType: 'email|phone',

  //Determine the rules for contact based on the value of contactType
  contact: s.match('contactType', {
    email: 'email!', // when contactType=email
    phone: 'string:11!', // when contactType=phone
    _default: 'string' // other situations
  })
});

Handling non-English values: If the condition value contains Chinese, numbers or special characters, just add quotes to the key name:

discount: s.match('level', {
  'Normal user': 'number:0-5',
  'VIP-1':   'number:0-20',
  '100':     'number:0-50'
})

2. s.if (simple condition)

Suitable for simple two-choice scenarios.

grammar:

s.if(conditionField, thenSchema, elseSchema)

Example:

const schema = s({
  isVip: 'boolean',

  // If it is VIP, discount 0-50, otherwise 0-10
  discount: s.if('isVip', 'number:0-50', 'number:0-10')
});

Advanced usage

1. Chain call

⚠️ .custom() supports synchronous custom logic; the asynchronous custom validator that returns Promise should be executed through validateAsync().

const schema = s({
  username: s('string:3-32!').pattern(/^[a-zA-Z0-9_]+$/)
    .label('username')
    .messages({
      'pattern': 'can only contain letters, numbers and underscores'
    }),

  email: s('email!').label('email address')
    .description('Used to log in and receive notifications')
    .custom((value) => {
      if (value.endsWith('@blocked.example')) return 'This email domain name is not allowed to be registered';
    })
});

2. Default validator

const schema = s({
  username: s('string!').username('5-20'), // Automatic regularization + length
  phone: s('string!').phone('cn'), // China mobile phone number
  password: s('string!').password('strong') // Strong password
});

Things to note

1. Condition validation

⚠️ Note: DSL strings do not support direct writing of conditional logic

'string | number' // ❌ not supported

Solution: Use s.match (recommended)

// ✅ Recommendation: use s.match
const schema = s({
  vipLevel: 'string',
  discount: s.match('vipLevel', {
    gold:   'number:0-50',
    silver: 'number:0-20',
    normal: 'number:0-5'
  })
});

2. Array constraints

Recommended: Use concise DSL syntax

'array!1-10<string:1-20>' // 1-10 elements, 1-20 characters each

Object items can also be written as a DSL object:

s.array({
  name: 'string!',
  age: 'number:18-'
}).min(1).max(10)

⚠️ Also: Use the complete JSON Schema format when you need direct JSON Schema interoperability

{
  type: 'array',
  items: { type: 'string' },
  minItems: 1,
  maxItems: 10
}

3. Regularity validation

⚠️ Note: DSL strings do not support writing regular rules directly

'string:/^[a-z]+$/' // ❌ Not supported

Solution: Use .pattern() method

s('string!').pattern(/^[a-z]+$/) // ✅ Recommended

4. Custom validation

⚠️ Note: DSL strings do not support custom logic

'string!@custom' // ❌ Not supported

Solution: Use .custom() method to carry custom logic; synchronization logic can be used validate() / validateAsync(), Promise-returning logic must use validateAsync()

s('string!').custom((value) => {
  // Custom synchronization logic
  if (value === 'reserved') {
    return 'The value is not available';
  }
})

Asynchronous validation (such as database duplication checking) can be placed in Promise-returning .custom() and executed through validateAsync(); it can also be executed separately in the business layer after the structure validation passes.


5. Detailed definition of object array

⚠️ Note: DSL abbreviation does not support detailed definition of object arrays

'array<object{name:string,age:number}>' // ❌ Not supported

Solution: Use s.array({ ... }) or .items({ ... })

const schema = s({
  users: s.array({
    name: 'string!',
    age: 'number:18-'
  }).min(1)
});

const usersField = s.array().items({
  name: 'string!',
  age: 'number:18-'
});

Do not write a field name such as items:array to declare an array type. In schema-dsl, the field name syntax describes the field name itself and required markers such as 'profile!'; the field type belongs in the value:

const schema = s({
  items: s.array({
    name: 'string!',
    price: 'number:0-10000!'
  })
});

Complete example

User registration form

import { s } from 'schema-dsl/pure';

const schema = s({
  //Basic information
  username: s('string:3-32!').username().label('username'),
  password: s('string!').password('strong').label('password'),
  email: s('email!').label('mailbox'),
  phone: s('string!').phone('cn').label('mobile number'),

  // personal data
  'profile!': {
    realName: 'string:2-50',
    gender: 'male|female|other',
    birthday: 'date',
    bio: 'string:500'
  },

  //Address information
  addresses: s.array({
    city: 'string!',
    street: 'string!'
  }).min(1).max(5), // 1-5 addresses

  // Label
  tags: 'array:1-10<string:1-20>', // 1-10 tags, each 1-20 characters

  // Agree to the terms
  agree: 'boolean!'
});

E-commerce product Schema

const schema = s({
  // Basic product information
  title: 'string:1-100!',
  price: 'number:0-!',
  stock: 'integer:0-',
  status: 'on_sale|off_sale|sold_out!',

  // product details
  'details!': {
    description: 'string:10000',
    images: 'array!1-10<url>',
    specs: s.array({
      name: 'string!',
      value: 'string!'
    }),
    tags: 'array:1-20<string:1-30>'
  },

  // SKU information
  skus: s.array({
    sku_code: 'string!',
    price: 'number!',
    stock: 'integer!'
  }).min(1)
});

API request validation

const schema = s({
  // Query parameters
  page: 'integer:1-',
  pageSize: 'integer:10-100',
  keyword: 'string:1-50',

  // filter conditions
  filters: {
    category: 'array<string>',
    priceRange: {
      min: 'number:0-',
      max: 'number:0-'
    },
    status: 'active|inactive'
  },

  // sort
  sort: {
    field: 'price|created_at|sales',
    order: 'asc|desc'
  }
});

FAQ

Q1: Why was the abbreviation function removed?

A: To reduce learning costs and reduce ambiguity. Using the full type name is more clear, especially for newbies.

Q2: How to write array length constraints?

A: Support writing directly in DSL:

'array!1-10<string>' // Recommended

Q3: How to define an object array?

A: Use s.array({ ... }); each field inside the object item keeps using normal DSL:

const schema = s({
  users: s.array({
    name: 'string!',
    email: 'email!'
  })
});

Q4: Doesn’t it support conditional validation?

A: Supported. It is recommended to use s.match:

s.match('vipLevel', { gold: 'number:0-50', silver: 'number:0-20' })

Q5: Can I use regular validation?

A: Yes, use .pattern() method:

s('string!').pattern(/^[a-z]+$/)


Corresponding sample file

Example entry: dsl-syntax.ts Description: Covers the basic types, constraints, enumerations, arrays and nested object writing methods of DSL syntax in Batch 1, and can be run directly for reference.