Complex validation scenarios including alternative validation, exact field checking, and conditional validation patterns for sophisticated validation logic.
Requires at least one of the provided validation chains to pass. Useful for scenarios where multiple validation approaches are acceptable.
/**
* Creates middleware requiring at least one validation to pass
* @param chains - Array of validation chains or nested arrays
* @param options - Configuration options including message and error type
* @returns ContextRunner for middleware execution
*/
function oneOf(
chains: (ValidationChain | ValidationChain[])[],
options?: OneOfOptions
): ContextRunner;
type OneOfOptions = {
message?: AlternativeMessageFactory | ErrorMessage;
errorType?: 'grouped' | 'least_errored' | 'flat';
};
type AlternativeMessageFactory = (nestedErrors: ValidationError[]) => any;Usage Examples:
import { oneOf, body, query } from "express-validator";
// Either email or username is required
app.post('/login', [
oneOf([
body('email').isEmail(),
body('username').isLength({ min: 3 })
], 'Either email or username is required'),
body('password').notEmpty()
], loginHandler);
// Multiple contact methods
app.post('/contact', [
oneOf([
body('email').isEmail(),
body('phone').isMobilePhone('any'),
body('socialMedia').isURL()
], 'At least one contact method required')
], contactHandler);
// Flexible ID formats
app.get('/user/:id', [
oneOf([
param('id').isUUID(),
param('id').isEmail(),
param('id').isAlphanumeric().isLength({ min: 3, max: 20 })
], 'Invalid user identifier')
], getUserHandler);Ensures only specified fields are present in the request, preventing unexpected fields from being processed.
/**
* Creates middleware ensuring only specified fields are present
* @param knownFields - Array of allowed field names
* @param options - Configuration options
* @returns ContextRunner for middleware execution
*/
function checkExact(
knownFields: string[],
options?: {
message?: string | UnknownFieldMessageFactory;
locations?: Location[];
}
): ContextRunner;
type UnknownFieldMessageFactory = (fields: UnknownFieldInstance[]) => any;
interface UnknownFieldInstance {
path: string;
location: Location;
}Usage Examples:
import { checkExact, body } from "express-validator";
// Strict API endpoint - only allow specific fields
app.post('/api/user', [
checkExact(['name', 'email', 'age'], {
message: 'Only name, email, and age fields are allowed'
}),
body('name').trim().notEmpty(),
body('email').isEmail(),
body('age').isInt({ min: 18 })
], createUserHandler);
// Check specific locations only
app.put('/settings', [
checkExact(['theme', 'language', 'notifications'], {
locations: ['body'],
message: 'Invalid settings field provided'
}),
body('theme').isIn(['light', 'dark']),
body('language').isLocale(),
body('notifications').isBoolean()
], updateSettingsHandler);
// Custom error message with field details
app.post('/product', [
checkExact(['name', 'price', 'category'], {
message: (unknownFields) => {
const fieldNames = unknownFields.map(field => field.path);
return `Unexpected fields: ${fieldNames.join(', ')}`;
}
})
], createProductHandler);Apply validation rules conditionally based on request context or other field values.
interface ValidationChain {
/**
* Apply validation conditionally
* @param condition - Function returning boolean for conditional application
* @returns ValidationChain for continued chaining
*/
if(condition: (value: any, meta: Meta) => boolean): ValidationChain;
}Usage Examples:
import { body } from "express-validator";
// Conditional required fields
app.post('/order', [
body('orderType').isIn(['pickup', 'delivery']),
// Address required only for delivery
body('address')
.if((value, { req }) => req.body.orderType === 'delivery')
.notEmpty()
.withMessage('Address required for delivery orders'),
// Phone required only for pickup
body('phone')
.if((value, { req }) => req.body.orderType === 'pickup')
.isMobilePhone('any')
.withMessage('Phone required for pickup orders')
], orderHandler);
// Conditional validation based on user role
app.post('/admin/user', [
body('role').isIn(['user', 'admin', 'moderator']),
// Admin-specific fields
body('permissions')
.if((value, { req }) => req.body.role === 'admin')
.isArray({ min: 1 })
.withMessage('Admin role requires permissions'),
// Moderator-specific fields
body('categories')
.if((value, { req }) => req.body.role === 'moderator')
.isArray({ min: 1 })
.withMessage('Moderator role requires categories')
], createUserHandler);Complex alternative validation with nested validation groups.
// Payment method validation with nested alternatives
app.post('/payment', [
body('method').isIn(['card', 'bank', 'wallet']),
oneOf([
// Credit card payment
[
body('method').equals('card'),
body('cardNumber').isCreditCard(),
body('expiryMonth').isInt({ min: 1, max: 12 }),
body('expiryYear').isInt({ min: new Date().getFullYear() }),
body('cvv').isLength({ min: 3, max: 4 })
],
// Bank transfer
[
body('method').equals('bank'),
body('bankCode').isLength({ min: 3, max: 11 }),
body('accountNumber').isNumeric(),
body('routingNumber').isLength({ min: 9, max: 9 })
],
// Digital wallet
[
body('method').equals('wallet'),
body('walletId').isUUID(),
body('pin').isLength({ min: 4, max: 6 }).isNumeric()
]
], 'Invalid payment information for selected method')
], paymentHandler);Validate fields that depend on combinations of other fields.
app.post('/shipping', [
body('method').isIn(['standard', 'express', 'overnight']),
body('weight').isFloat({ min: 0.1 }),
body('dimensions.length').isFloat({ min: 1 }),
body('dimensions.width').isFloat({ min: 1 }),
body('dimensions.height').isFloat({ min: 1 }),
// Express shipping restrictions
body('weight')
.if((value, { req }) => {
return req.body.method === 'express' && value > 50;
})
.custom(() => {
throw new Error('Express shipping limited to 50lbs');
}),
// Overnight shipping restrictions
oneOf([
[
body('method').not().equals('overnight')
],
[
body('method').equals('overnight'),
body('weight').isFloat({ max: 25 }),
body('dimensions.length').isFloat({ max: 24 }),
body('dimensions.width').isFloat({ max: 18 }),
body('dimensions.height').isFloat({ max: 12 })
]
], 'Overnight shipping has size/weight restrictions')
], shippingHandler);Perform validation while maintaining state or making external calls.
app.post('/reservation', [
body('eventId').isUUID(),
body('attendees').isInt({ min: 1, max: 10 }),
// Check availability with side effects
body('eventId').custom(async (eventId, { req }) => {
const event = await Event.findById(eventId);
if (!event) {
throw new Error('Event not found');
}
const requestedAttendees = req.body.attendees;
if (event.availableSpots < requestedAttendees) {
throw new Error(`Only ${event.availableSpots} spots available`);
}
// Store event in request for later use
req.event = event;
return true;
}),
// Validate based on stored event data
body('attendees').custom((attendees, { req }) => {
if (req.event && req.event.maxPerReservation < attendees) {
throw new Error(`Maximum ${req.event.maxPerReservation} attendees per reservation`);
}
return true;
})
], reservationHandler);Handle complex error scenarios with custom error formatting.
// Custom error handling for oneOf
app.post('/flexible-auth', [
oneOf([
body('email').isEmail(),
body('phone').isMobilePhone('any'),
body('username').isAlphanumeric().isLength({ min: 3 })
], (nestedErrors) => {
// Custom error message based on what was attempted
const attempts = nestedErrors.map(error => error.path).join(', ');
return `Invalid credentials. Tried validating: ${attempts}`;
}),
body('password').isLength({ min: 6 })
], authHandler);
// Exact validation with detailed error reporting
app.post('/strict-api', [
checkExact(['name', 'email'], {
message: (unknownFields) => {
const fields = unknownFields.map(field =>
`${field.path} (in ${field.location})`
);
return {
error: 'UNEXPECTED_FIELDS',
message: 'Request contains unexpected fields',
fields: fields
};
}
})
], strictApiHandler);