Chained Condition API - ConditionalBuilder

Overview

ConditionalBuilder Provides a smooth chained conditional judgment API, similar to JavaScript’s if-else statement, which is used to dynamically adjust validation rules based on actual data during validation.

Key semantics: When you use the "return on failure" mode such as .message() / .assert() / .check(), the conditional function should be written as failure condition, because the condition will be judged as failure if it returns true.

Core features

  • Chain Call - Fluent API, similar to JavaScript if-else
  • Runtime execution - judge based on actual data during validation
  • Multiple condition combination - supports and/or logical combination
  • 🆕 Independent Message - v1.1.1+ Each.and()/.or() can have independent error message
  • else optional - If you don’t write else, it will not be validated.
  • Simplified Design - message automatically throws errors, no need to throwError()
  • Fully Compatible - Does not affect existing APIs

🆕 v1.1.1 new features

Independent message support - .and()/.or() can be called later .message()

Each condition can have its own error message

Starting from v1.1.1, it is supported to call .message() after .and() and .or() to set an independent error message to make the error prompt more precise.

Basic usage

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

// ✅ v1.1.1+ new feature: independent message for each condition
s.if(d => !d)
  .message('ACCOUNT_NOT_FOUND')
  .and(d => d.tradable_credits < amount)
  .message('INSUFFICIENT_TRADABLE_CREDITS')
  .assert(account);

// How it works:
// - first condition is true → return 'ACCOUNT_NOT_FOUND'
// - second condition is true → return 'INSUFFICIENT_TRADABLE_CREDITS'
// - all conditions are false → validation successful

Multiple.and() conditions

//Multiple layers of validation, with clear error messages for each layer
s.if(d => !d)
  .message('ACCOUNT_NOT_FOUND')
  .and(d => d.status !== 'active')
  .message('ACCOUNT_INACTIVE')
  .and(d => d.tradable_credits < amount)
  .message('INSUFFICIENT_TRADABLE_CREDITS')
  .assert(account);

// Check in sequence, the first true condition returns its message

.or() conditionally independent messages

// OR conditions also support independent messages
s.if(d => d.age < 18)
  .message('Underage users cannot register')
  .or(d => d.isBlocked)
  .message('Account has been banned')
  .assert(data);

// If any condition is true, it fails and the corresponding message is returned.

chain check mode

v1.1.1 introduced Chain Check Mode, which is automatically enabled when the following conditions are met:

  1. Use .message() mode (not .then()/.else())
  2. The root condition is .message()
  3. There are .and() conditions
  4. No .or() condition

Chain check mode features:

  • Check each condition in turn
  • The first condition true fails and its message is returned
  • Validation passes when all conditions are false
  • This mode is a sequential failure-check chain, not an A && B boolean expression

Example comparison:

// ✅ Enable chain checking (pure AND scenario)
s.if(d => !d).message('A').and(d => d < 100).message('B')

// ❌ Not enabled (has.or(), uses traditional AND/OR logic)
s.if(d => !d).message('A').and(d => d < 100).or(d => d > 200).message('B')

// ❌ Not enabled (use.then()/.else(), not message mode)
s.if(d => d.age >= 18).and(d => d.role === 'admin').then('email!')

backward compatibility

100% backward compatible, does not affect existing code:

// ✅Original usage continues to work
s.if(d => d.age >= 18).and(d => d.role === 'admin').message('does not meet the conditions')

// ✅ It’s okay not to call.message() after.and()
s.if(d =>!d).message('Overall error').and(d => d < 100).assert(50)
// → Use global message 'Global error'

Practical application scenarios

Scenario 1: Account Validation

function validateAccount(account, amount) {
  s.if(d => !d)
    .message('ACCOUNT_NOT_FOUND')
    .and(d => d.status !== 'active')
    .message('ACCOUNT_INACTIVE')
    .and(d => d.balance < amount)
    .message('INSUFFICIENT_BALANCE')
    .assert(account);
}

// Each failure point has a clear error message

Scenario 2: User permission validation

function validateUserPermission(user) {
  s.if(d => d.role !== 'admin')
    .message('NO_ADMIN_PERMISSION')
    .and(d => !d.isVerified)
    .message('USER_NOT_VERIFIED')
    .and(d => d.isBanned)
    .message('USER_BANNED')
    .assert(user);
}

Scenario 3: Order status check

function validateOrder(order) {
  s.if(d => d.status !== 'paid')
    .message('ORDER_NOT_PAID')
    .and(d => !d.payment)
    .message('PAYMENT_INFO_MISSING')
    .and(d => !d.shippingAddress)
    .message('SHIPPING_ADDRESS_MISSING')
    .assert(order);
}

Differences from existing methods

s.if() provides two usage methods, automatically selected according to the parameter type:

WayParameter typeexecution timinguseExample
Method 1stringWhen Schema is definedstatic boolean conditions.if('isVip', thenSchema, elseSchema)
Method 2functionWhen verifyingDynamic condition judgments.if((data) => data.age >= 18).then(...)

Method 1 (field conditions): static judgment based on field values

// Example: Select different validation rules based on isVip field value
s.if('isVip', 'number:0-50', 'number:0-10')

Method 2 (function condition): dynamic judgment based on complete data

// Example: Dynamic selection based on the combination logic of multiple fields
s.if((data) => data.age >= 18 && data.role === 'admin')
  .then('email!')
  .else('email')

In addition, s.match() is suitable for multi-value mapping scenarios:

// Example: Map different validation rules based on type field value
s.match('type', {
  email: 'email!',
  phone: 'string:11!',
  _default: 'string'
})

quick start

Basic usage

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

// Method 1: Traditional method (requires validate function)
const schema1 = s({
  age: 'number!',
  status: s.if((data) => data.age < 18)
    .message('Underage users cannot register')
});

validate(schema1, { age: 16, status: 'active' });
// => { valid: false, errors: [{ message: 'Underage users cannot register' }], data: { age: 16, status: 'active' } }

// ✅ Method 2: Shortcut (one line of code validation)
const result = s.if((data) => data.age < 18)
  .message('Underage users cannot register')
  .validate({ age: 16 });
// => { valid: false, errors: [{ message: 'Underage users cannot register' }], data: { age: 16 } }

// ✅ Method 3:.check() quick judgment
const isValid = s.if((data) => data.age < 18)
  .message('Underage users cannot register')
  .check({ age: 16 });
// => false

// 2. Condition + then/else (dynamic Schema)
const result = s.if((data) => data.userType === 'admin')
  .then('email!') // Required by administrator
  .else('email') // Optional for ordinary users
  .validate({ userType: 'admin', email: 'admin@example.com' });

// 3. else optional
const result = s.if((data) => data.userType === 'vip')
  .then('enum:gold|silver|bronze!')
  // Do not write else, non-vip users will not be validated
  .validate({ userType: 'user' });

// 4. Reuse validator
const ageValidator = s.if(d => d.age < 18).message('Underage users cannot register');
const r1 = ageValidator.validate({ age: 16 }); // failed
const r2 = ageValidator.validate({ age: 20 }); // Pass

Multiple condition combinations

// 1. AND condition
const result = s.if((data) => data.age >= 18)
  .and((data) => data.userType === 'admin')
  .message('Only adult administrators can operate')
  .validate({ age: 20, userType: 'user' });

// 2. OR condition
const result = s.if((data) => data.age < 18)
  .or((data) => data.status === 'blocked')
  .message('Registration not allowed')
  .validate({ age: 16, status: 'active' });

// 3. Complex combination
const result = s.if((data) => data.age >= 18)
  .and((data) => data.userType === 'admin')
  .or((data) => data.status === 'vip')
  .then('email!')
  .else('email')
  .validate(data);

elseIf branch

const validator = s.if((data) => data.userType === 'admin')
  .then('array<string>!')
  .elseIf((data) => data.userType === 'vip')
  .then('array<string>')
  .elseIf((data) => data.userType === 'user')
  .then('array')
  .else(null); // Visitors do not verify

const r1 = validator.validate({ userType: 'admin', permissions: ['read', 'write'] });
const r2 = validator.validate({ userType: 'vip' });
const r3 = validator.validate({ userType: 'guest' });

API reference

s.if(condition)

Create a chained conditional builder. Parameters:

  • condition {Function} - conditional function, receiving complete data object
    • Parameter: (data: any) => boolean
    • Return: boolean - ​​true means the condition is met

Returns: ConditionalBuilder - ​​builder instance

Example:

s.if((data) => data.age >= 18)
s.if((data) => data.userType === 'admin')
s.if((data) => data.status === 'active' && data.validated)

.and(condition)

Add an AND condition (combine with the previous condition).

v1.1.1+ Support calling .message() after .and() to set independent error messages Parameters:

  • condition {Function} - conditional function

Return: this - ​​supports chain calls

Basic example:

// Traditional usage: all conditions share one message
s.if((data) => data.age >= 18)
  .and((data) => data.userType === 'admin')
  .message('Does not meet conditions')

v1.1.1+ Standalone Message:

// ✅ Each condition has its own error message
s.if((data) => !data)
  .message('Account does not exist')
  .and((data) => data.balance < 100)
  .message('Insufficient balance')
  .assert(account);

// How it works:
// - The first condition is true → return 'account does not exist'
// - The second condition is true → return 'Insufficient balance'
// - all conditions are false → validation successful

Multiple.and() conditions:

// Supports multiple.and() conditions, each with independent messages
s.if(d => !d)
  .message('NOT_FOUND')
  .and(d => d.status !== 'active')
  .message('INACTIVE')
  .and(d => d.balance < 100)
  .message('INSUFFICIENT')
  .assert(account);

// Check in sequence, the first true condition returns its message

logic:

  • Traditional mode: (condition1 AND condition2) - ​​all conditions must be true to fail
  • Chain check mode (v1.1.1+): Check in sequence, the first one that is true fails

Chain check mode trigger conditions:

  1. Use .message() mode
  2. The root condition is .message()
  3. There are .and() conditions
  4. No .or() condition

.or(condition)

Add an OR condition (combine with the previous condition).

v1.1.1+ Support calling .message() after .or() to set independent error messages Parameters:

  • condition {Function} - conditional function

Return: this - ​​supports chain calls

Basic example:

// Traditional usage: all conditions share one message
s.if((data) => data.age < 18)
  .or((data) => data.status === 'blocked')
  .message('Registration not allowed')

v1.1.1+ Standalone Message:

// ✅ Each OR condition has its own error message
s.if(d => d.age < 18)
  .message('Underage users cannot register')
  .or(d => d.isBlocked)
  .message('Account has been banned')
  .assert(data);

// How it works:
// - The first condition is true → return 'Underage users cannot register'
// - The second condition is true → returns 'Account has been banned'
// - all conditions are false → validation successful

Logic: (condition1 OR condition2) - ​​Fails if any condition is true

Notice:

  • Chained check mode will not be enabled if .or() condition is present
  • Use traditional AND/OR combinational logic

.build()

Output the current ConditionalBuilder into a schema object that can be directly passed to Validator / validate() for use.

.build() is an alias of .toSchema(), suitable for use when you want to get the final schema explicitly.

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

const conditionalSchema = s.if(data => data.age >= 18)
  .then('email!')
  .else('email')
  .build();

const result = validate(conditionalSchema, 'user@example.com');
console.log(result.valid);

.elseIf(condition)

Add else-if branch. Parameters:

  • condition {Function} - conditional function

Return: this - ​​supports chain calls

Example:

s.if((data) => data.userType === 'admin')
  .then('email!')
  .elseIf((data) => data.userType === 'vip')
  .then('email')
  .else(null)

Note: Must be called after .if()


.message(msg)

Set error message (supports multi-language keys).

v1.1.1+ Support setting independent messages for .and() and .or() conditions Parameters:

  • msg {string} - error message or multilingual key

Return: this - ​​supports chain calls

Behavior: This error is automatically thrown when the condition is true (no need for .throwError())

Basic example:

s.if((data) => data.age < 18)
  .message('Underage users cannot register')

//Support multi-language key
s.if((data) => data.age < 18)
  .message('error.underage')

v1.1.1+ Set independent message for.and():

// ✅ Each condition has its own error message
s.if((data) => !data)
  .message('Account does not exist')
  .and((data) => data.balance < 100)
  .message('Insufficient balance')
  .assert(account);

v1.1.1+ Set independent message for.or():

// ✅ OR conditions also support independent messages
s.if(d => d.age < 18)
  .message('underage')
  .or(d => d.isBlocked)
  .message('Banned')
  .assert(data);

Chain check mode description (v1.1.1+):

Chained check mode is automatically enabled when the following conditions are met:

  1. Use .message() mode (not .then()/.else())
  2. The root condition is .message()
  3. There are .and() conditions
  4. No .or() condition
// ✅ Enable chain checking (pure AND scenario)
s.if(d => !d).message('A').and(d => d < 100).message('B')

// ❌ Not enabled (with.or())
s.if(d => !d).message('A').and(d => d < 100).or(d => d > 200).message('B')

// ❌ Not enabled (use.then()/.else())
s.if(d => d.age >= 18).and(d => d.role === 'admin').then('email!')

.then(schema)

Set the Schema when the conditions are met. Parameters:

  • schema {string|DslBuilder|JSONSchema} - DSL string or Schema object

Return: this - ​​supports chain calls

Example:

// DSL string
s.if((data) => data.userType === 'admin')
  .then('email!')

// DslBuilder instance
s.if((data) => data.userType === 'admin')
  .then(s('email!').label('Administrator's email'))

// JSON Schema object
s.if((data) => data.userType === 'admin')
  .then({ type: 'string', format: 'email' })

.else(schema)

Set the default Schema (when all conditions are not met). Parameters:

  • schema {string|DslBuilder|JSONSchema|null} - DSL string, Schema object, or null

Return: this - ​​supports chain calls

Feature: Optional, if you don’t write else, it won’t be validated.

Example:

//Explicitly specify else
s.if((data) => data.userType === 'admin')
  .then('email!')
  .else('email')

// else is null (explicitly skip validation)
s.if((data) => data.userType === 'admin')
  .then('email!')
  .else(null)

// Don't write else (implicitly skip validation)
s.if((data) => data.userType === 'admin')
  .then('email!')

.validate(data, options)

Quick validation method - returns complete validation results. Parameters:

  • data {*} - Data to be validated (any type)
  • options {Object} - Authentication options (optional)
    • locale {string} - locale (e.g. 'zh-CN', 'en-US')
    • messages {Object} - Custom error message

Return: Object - ​​Validation result { valid, errors, data }

Feature: One line of code to complete validation, no external validate() function required

Example:

// One line of code validation
const result = s.if(d => d.age < 18)
  .message('Underage users cannot register')
  .validate({ age: 16 });
// => { valid: false, errors: [...], data }

//Reuse validator
const ageValidator = s.if(d => d.age < 18).message('underage');
const r1 = ageValidator.validate({ age: 16 });  // false
const r2 = ageValidator.validate({ age: 20 });  // true

//Support validation options
const result = s.if(d => d.age < 18)
  .message('conditional.underAge')
  .validate({ age: 16 }, { locale: 'zh-CN' });

// Validate non-object types
const result = s.if(d => d.includes('@'))
  .then('email!')
  .validate('test@example.com');

.validateAsync(data, options)

Asynchronous validation method - automatically throws an exception on failure. Parameters:

  • data {*} - Data to be validated
  • options {Object} - Authentication options (optional)

Return: Promise<*> - ​​If the validation passes, the data will be returned, if it fails, an exception will be thrown.

Throws: ValidationError - ​​Validation failure throws exception

Features: Suitable for async/await scenarios, automatically throwing errors on failure

Example:

// Asynchronous validation, automatically throwing an error if it fails
try {
  const data = await s.if(d => d.age < 18)
    .message('Underage users cannot register')
    .validateAsync({ age: 16 });
} catch (error) {
  console.log(error.message); // "Underage users cannot register"
  console.log(error.errors); // Detailed error information
}

// Express middleware
app.post('/register', async (req, res, next) => {
  try {
    await s.if(d => d.age < 18)
      .message('Underage users cannot register')
      .validateAsync(req.body);

    // Validation passed, continue processing...
    const user = await createUser(req.body);
    res.json(user);
  } catch (error) {
    next(error);
  }
});

//Reuse validator
const ageValidator = s.if(d => d.age < 18).message('underage');

try {
  await ageValidator.validateAsync({ age: 16 });
} catch (error) {
  // Handle errors
}

.assert(data, options)

Assertion method - synchronous validation, if it fails, an error will be thrown directly. Parameters:

  • data {*} - Data to be validated
  • options {Object} - Authentication options (optional)

Return: * - ​​Return data after validation

Throws: ValidationError - ​​Throws ValidationError directly when validation fails

Feature: Synchronous version of assertion validation, suitable for fast failure scenarios

Example:

// Assertion validation, if it fails, an error will be thrown directly
try {
  s.if(d => d.age < 18)
    .message('Underage users cannot register')
    .assert({ age: 16 });
} catch (error) {
  console.log(error.message); // "Underage users cannot register"
}

// Quick assertion in function
function registerUser(userData) {
  // Assertion validation
  s.if(d => d.age < 18)
    .message('Underage users cannot register')
    .assert(userData);

  s.if(d => !d.email)
    .message('Mailbox cannot be empty')
    .assert(userData);

  // Validation passed, continue processing...
  return createUser(userData);
}

// chained assertions
function validateAndCreate(data) {
  s.if(d => d.age < 18).message('underage').assert(data);
  s.if(d =>!d.email).message('Email required').assert(data);
  s.if(d =>!d.username).message('Username required').assert(data);

  return createUser(data);
}

.check(data)

Quick check method - returns only boolean. Parameters:

  • data {*} - Data to be validated

Return: boolean - ​​Validation passed

Features: More concise than .validate(), suitable for scenarios where only true and false needs to be determined

Example:

//Quick judgment
const isValid = s.if(d => d.age < 18)
  .message('underage')
  .check({ age: 16 });
// => false

// Assert scenario
if (!validator.check(userData)) {
  console.log('Validation failed');
}

// Loop validation
const users = [{ age: 16 }, { age: 20 }, { age: 17 }];
const adults = users.filter(u =>
  !s.if(d => d.age < 18).message('underage').check(u)
);

Usage scenarios

Scenario 1: User registration - quick validation

Use the .validate() method to quickly verify user registration data.

//Create a reusable validator
const validators = {
  age: s.if(d => d.age < 18).message('Underage users cannot register'),
  email: s.if(d => d.userType === 'admin')
    .message('The administrator must provide an email address')
};

// Quick validation (one line of code)
function registerUser(userData) {
  //Verify age
  const ageResult = validators.age.validate(userData);
  if (!ageResult.valid) {
    return { error: ageResult.errors[0].message };
  }

  //Verify email
  const emailResult = validators.email.validate(userData);
  if (!emailResult.valid) {
    return { error: emailResult.errors[0].message };
  }

  return { success: true };
}

// use
registerUser({ username: 'test', age: 16 });
// => { error: 'Underage users cannot register' }

Scenario 2: Batch data validation - using.check()

Use the .check() method to quickly filter qualified data.

const users = [
  { name: 'Zhang San', age: 16 },
  { name: 'Li Si', age: 20 },
  { name: 'Wang Wu', age: 17 },
  { name: 'Zhao Liu', age: 25 }
];

//Create validator
const canRegister = s.if(d => d.age < 18)
  .message('underage');

// ✅ Use.check() to filter
const validUsers = users.filter(u => !canRegister.check(u));
// => [{ name: 'Li Si', age: 20 }, { name: 'Zhao Liu', age: 25 }]

// ✅ Use.check() statistics
const minorCount = users.filter(u => canRegister.check(u)).length;
console.log(`Underage users: ${minorCount}`);
// => "Underage users: 2 people"

Scenario 3: Real-time validation of forms

// Front-end form validation
const formValidators = {
  username: s.if(d => d.length < 3)
    .message('Username must be at least 3 characters'),

  password: s.if(d => d.length < 8)
    .message('Password must be at least 8 characters')
};

// Real-time validation (when inputting)
function onUsernameChange(value) {
  const isValid = formValidators.username.check(value);
  if (!isValid) {
    showError('Username must be at least 3 characters');
  } else {
    clearError();
  }
}

// Submit validation
function onSubmit(formData) {
  const usernameResult = formValidators.username.validate(formData.username);
  const passwordResult = formValidators.password.validate(formData.password);

  if (!usernameResult.valid) {
    return alert(usernameResult.errors[0].message);
  }

  if (!passwordResult.valid) {
    return alert(passwordResult.errors[0].message);
  }

  // Submit form...
}

Scenario 4: User permission check

// Permission validator
const hasPermission = s.if(d => d.role === 'admin')
  .or(d => d.role === 'moderator')
  .message('Insufficient permissions');

// middleware
function checkPermission(req, res, next) {
  if (!hasPermission.check(req.user)) {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  next();
}

// routing
app.delete('/users/:id', checkPermission, deleteUser);

Scenario 5: Validate different fields based on age and user type (contrast with traditional methods)

// Traditional way (requires validate function)
const schema = s({
  username: 'string:3-32!',
  age: 'number:1-120!',
  userType: 'enum:admin|vip|user!',

  // Minors are prohibited from registering
  ageCheck: s.if((data) => data.age < 18)
    .message('Underage users cannot register'),

  //The administrator must have an email address
  email: s.if((data) => data.userType === 'admin')
    .then('email!')
    .else('email'),

  // VIP users must have a mobile phone number
  phone: s.if((data) => data.userType === 'vip')
    .then('string:11!')
    .else(null),

  // Administrators and VIPs can set nicknames
  nickname: s.if((data) => data.userType === 'admin')
    .or((data) => data.userType === 'vip')
    .then('string:2-20')
    .else(null)
});

// test
validate(schema, {
  username: 'admin1',
  age: 25,
  userType: 'admin',
  email: 'admin@example.com'
});
// => { valid: true }

Scenario 2: Product release

Validate different fields based on product type.

const schema = s({
  title: 'string:1-100!',
  price: 'number:0-!',
  type: 'enum:physical|digital|service!',

  // Physical products require weight and dimensions
  weight: s.if((data) => data.type === 'physical')
    .then('number:0-!')
    .else(null),

  dimensions: s.if((data) => data.type === 'physical')
    .then('string!')
    .else(null),

  // Digital products require a download link
  downloadUrl: s.if((data) => data.type === 'digital')
    .then('url!')
    .else(null),

  // Service class requires service duration
  duration: s.if((data) => data.type === 'service')
    .then('number:1-!')
    .else(null)
});

// physical goods
validate(schema, {
  title: 'Laptop',
  price: 5999,
  type: 'physical',
  weight: 1.5,
  dimensions: '30x20x2cm'
});
// => { valid: true }

//Digital goods
validate(schema, {
  title: 'e-book',
  price: 29.9,
  type: 'digital',
  downloadUrl: 'https://example.com/download'
});
// => { valid: true }

Scenario 3: Permission control

Control access based on user role and status.

const schema = s({
  userId: 'string!',
  role: 'enum:admin|moderator|user!',
  status: 'enum:active|suspended|banned!',

  // Banned users are prohibited from operating
  accessCheck: s.if((data) => data.status === 'banned')
    .message('Your account has been banned'),

  // Pause user can only view
  operationType: s.if((data) => data.status === 'suspended')
    .then('enum:view!')
    .else('enum:view|edit|delete!'),

  // Administrators can access all resources
  resourceIds: s.if((data) => data.role === 'admin')
    .then('array<string>') // optional
    .else('array<string>!') // required
});

best practices

1. Keep conditional functions simple

Not recommended:

s.if((data) => {
  const user = getUserFromDB(data.userId); // Synchronous database query
  return user.level > 5;
})

Recommended:

s.if((data) => data.userLevel > 5)

Reason: Conditional functions should only read data objects and should not have side effects or perform time-consuming operations.


2. Use meaningful field names

Not recommended:

const schema = s({
  field1: 'string!',
  check1: s.if((data) => data.field1 === 'admin')
    .message('Error')
});

Recommended:

const schema = s({
  userType: 'string!',
  ageVerification: s.if((data) => data.age < 18)
    .message('Underage users cannot register')
});

3. Fair use else

When conditions are not met and different validation rules are required, use .else():

s.if((data) => data.userType === 'admin')
  .then('email!')
  .else('email') // Different validation rules

When the conditions are not met, validation is not required and .else() is omitted:

s.if((data) => data.userType === 'vip')
  .then('string:6!')
  // Do not write else, non-vip users will not be validated

4. Use the internal logic of the function first for multiple condition combinations

Simple conditions can be composed directly inside functions:

// ✅ Recommended (concise)
s.if((data) => data.age >= 18 && data.userType === 'admin')
  .then('email!')

// ⚠️ Available but a little cumbersome
s.if((data) => data.age >= 18)
  .and((data) => data.userType === 'admin')
  .then('email!')

Use .and() / .or() when complex logic or maintainability is required:

// ✅ Recommended (high readability)
s.if((data) => data.age >= 18)
  .and((data) => data.userType === 'admin')
  .and((data) => data.validated)
  .or((data) => data.isSuperUser)
  .then('email!')

5. Error messages are clear and unambiguous

Not recommended:

s.if((data) => data.age < 18)
  .message('Error')

Recommended:

s.if((data) => data.age < 18)
  .message('Underage users cannot register')

BETTER (Supports multiple languages):

s.if((data) => data.age < 18)
  .message('error.user.underage')

FAQ

Q1: When will the conditional function be executed?

A: Executed when calling validate(), not when defining Schema.

const schema = s({
  email: s.if((data) => data.userType === 'admin')
    .then('email!') // ← will not be executed here
});

validate(schema, data); // ← conditional function is executed here

Q2: What data can the conditional function access?

A: Can access the complete data object.

const schema = s({
  age: 'number!',
  userType: 'string!',
  status: 'string!',
  email: s.if((data) => {
    // All fields can be accessed
    return data.age >= 18 && data.userType === 'admin' && data.status === 'active';
  }).then('email!')
});

Q3: How to deal with conditional functions throwing errors?

A: If the conditional function throws an error, it will be captured and regarded as the condition is not met.

const schema = s({
  obj: 'object!',
  result: s.if((data) => data.obj.nested.value > 10)
    .then('string!')
    .else(null)
});

// data.obj.nested does not exist, access will throw an error
validate(schema, { obj: {} });
// => { valid: true } If the condition is not met, execute else(null)

Suggestion: Do defensive checks in conditional functions:

s.if((data) => data.obj?.nested?.value > 10)
  .then('string!')

Q4: Can s.if() be nested?

A: Yes, nesting is supported.

const schema = s({
  userType: 'string!',
  age: 'number!',
  email: s.if((data) => data.userType === 'admin')
    .then(
      s.if((data) => data.age >= 18)
        .then('email!')
        .else('email')
    )
    .else('email')
});

Q5: How to use it with the existing s.match() method?

A: Can be mixed, choose the most suitable method.

const schema = s({
  // Static value mapping - use match
  userType: 'enum:admin|vip|user!',
  level: s.match('userType', {
    admin: 'enum:high!',
    vip: 'enum:medium!',
    user: 'enum:low!'
  }),

  // Dynamic conditional judgment - use if
  email: s.if((data) => data.userType === 'admin' && data.level === 'high')
    .then('email!')
    .else('email')
});

Selection Suggestions:

  • Simple Value Mapping → Use s.match()
  • Complex conditional logic → Use s.if()

Q6: Are non-object types (strings, arrays, numbers, etc.) supported?

A: Fully support! Any type of value can be validated directly.

//Example 1: Directly verify the string
const stringSchema = s.if((data) => typeof data === 'string' && data.includes('@'))
  .then('email!')
  .else('string:1-50');

validate(stringSchema, 'test@example.com'); // ✅ as email validation
validate(stringSchema, 'just a text'); // ✅ Validate as a normal string

//Example 2: Directly verify the array
const arraySchema = s.if((data) => Array.isArray(data) && data.length > 5)
  .message('Array can have up to 5 elements');

validate(arraySchema, [1, 2, 3]); // ✅ Passed
validate(arraySchema, [1, 2, 3, 4, 5, 6]); // ❌ failed

// Example 3: Verify numbers directly
const numberSchema = s.if((data) => typeof data === 'number' && data < 0)
  .message('Negative numbers are not allowed');

validate(numberSchema, 10); // ✅ Passed
validate(numberSchema, -5); // ❌ failed

//Example 4: Automatically identify the type (email or mobile phone number)
const contactSchema = s.if((data) => typeof data === 'string' && data.includes('@'))
  .then('email!')
  .else('string:11!');

validate(contactSchema, 'user@example.com'); // ✅ as email validation
validate(contactSchema, '13800138000'); // ✅ Verify as mobile phone number

Full Example: See test/unit/conditional-non-object.test.ts


Q7: How is the performance?

A: Excellent performance, conditional function execution is very fast.

  • The conditional function is a pure JavaScript function and executes quickly
  • Traverse the condition chain only once and stop when the first matching condition is found.
  • Support cache optimization (WeakMap)

Performance Tips:

  • Avoid performing time-consuming operations (database queries, API calls) in conditional functions
  • Put the most common conditions first (if instead of elseIf)

Change log

v1.1.1 (2026-01-05)

  • ✅ Added ConditionalBuilder category
  • ✅ Added s.if() chain condition API
  • ✅Supports and/or multiple condition combinations
  • ✅ Support elseIf multiple branches
  • ✅ message automatically throws errors (no need to throwError)
  • ✅ else optional (if you don’t write it, it won’t be validated)
  • ❌ Remove invalid old conditional method type definitions


Corresponding sample file

Example entry: conditional-api.ts Description: Also overrides .check() / .assert() in failed predicate mode, as well as field name version s.if(field, then, else) and s.match() mapping.