Validator API
The Validator is the runtime validation engine created by the Builder. It provides methods to validate data and handle results using a functional Result pattern.
Overview
A Validator instance is created by calling .build()
on a configured Builder. The validator provides two main methods:
- validate() - Validates data and returns original values
- parse() - Validates data and returns transformed values
Both methods return a Result<T>
object that provides safe, functional error handling.
Creating a Validator
import { Builder } from '@maroonedog/luq/core';
import {
requiredPlugin,
stringMinPlugin,
numberMinPlugin,
transformPlugin
} from '@maroonedog/luq/plugin';
// Define your type
type UserData = {
name: string;
age: number;
email: string;
};
// Create validator using Builder
const validator = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.use(numberMinPlugin)
.for<UserData>()
.v('name', b => b.string.required().min(2))
.v('age', b => b.number.required().min(18))
.v('email', b => b.string.required())
.build(); // Returns a Validator instance
Validation Methods
validate() Method
The validate()
method checks if data conforms to the schema and returns the original values without applying any transformations:
// validate() returns original values (no transformations applied)
const result = validator.validate({
name: 'John Doe',
age: 25,
email: 'john@example.com'
});
// Check if validation passed
if (result.isValid()) {
const data = result.unwrap();
console.log('Valid data:', data);
// data type is UserData with original values
} else {
console.log('Validation errors:', result.errors);
}
// Using the 'valid' property (backward compatibility)
if (result.valid) {
console.log('Valid!');
}
💡 When to use validate()
- • When you only need to check if data is valid
- • When you want to preserve original values
- • When transformations are not needed
- • For validation-only scenarios
parse() Method
The parse()
method validates data AND applies all transformations, returning the transformed values:
// Define a validator with transformations
const transformValidator = Builder()
.use(requiredPlugin)
.use(transformPlugin)
.for<{ email: string; age: string }>()
.v('email', b => b.string.required()
.transform(email => email.toLowerCase().trim()))
.v('age', b => b.string.required()
.transform(age => parseInt(age, 10)))
.build();
// parse() applies transformations and returns transformed values
const result = transformValidator.parse({
email: ' JOHN@EXAMPLE.COM ',
age: '25'
});
if (result.isValid()) {
const data = result.unwrap();
// data.email is 'john@example.com' (lowercased and trimmed)
// data.age is 25 (number, not string)
}
📝 When to use parse()
- • When you have transform plugins in your validator
- • When you need normalized/sanitized data
- • When converting types (e.g., string to number)
- • For data processing pipelines
pick() Method
The pick()
method extracts a single field validator from a built validator, enabling individual field validation:
// Extract single field validator from a built validator
const userValidator = Builder()
.use(requiredPlugin)
.use(stringEmailPlugin)
.use(stringMinPlugin)
.use(numberMinPlugin)
.for<UserProfile>()
.v('email', b => b.string.required().email())
.v('username', b => b.string.required().min(3))
.v('age', b => b.number.required().min(18))
.v('profile.bio', b => b.string.optional().max(500))
.build();
// Use pick() to extract a single field validator
const emailValidator = userValidator.pick('email');
const ageValidator = userValidator.pick('age');
const bioValidator = userValidator.pick('profile.bio'); // Nested fields supported
// Validate individual fields
const emailResult = emailValidator.validate('test@example.com');
if (emailResult.valid) {
console.log('Valid email:', emailResult.value);
}
const ageResult = ageValidator.validate(25);
if (ageResult.valid) {
console.log('Valid age:', ageResult.value);
}
// Picked validators are type-safe
// emailValidator is FieldValidator<UserProfile, string>
// ageValidator is FieldValidator<UserProfile, number>
// bioValidator is FieldValidator<UserProfile, string | undefined>
// Can provide context (other field values) for field dependencies
const bioResult = bioValidator.validate('My bio text', {
username: 'john_doe',
age: 25
});
🎯 When to use pick()
- • When you need to validate individual fields in isolation
- • For real-time field validation in forms
- • When testing specific field validation logic
- • As an alternative to Plugin Registry for single field validation
💡 Tip: If you already have a validator, use pick()
instead of creating a separate Plugin Registry for single field validation.
Result Object
Both validation methods return a Result<T>
object that encapsulates the validation outcome. This provides a type-safe, functional approach to error handling.
Result Methods
const result = validator.validate(inputData);
// Check validity
if (result.isValid()) {
// Successfully validated
}
if (result.isError()) {
// Validation failed
}
// Get data with different strategies
const data1 = result.unwrap(); // Throws if invalid
const data2 = result.unwrapOr(defaultData); // Returns default if invalid
const data3 = result.unwrapOrElse(
errors => computeDefault(errors) // Lazy default computation
);
// Direct access (use with caution)
const maybeData = result.data(); // T | undefined
const errors = result.errors; // ValidationError[]
Error Handling
When validation fails, the Result object provides access to detailed error information:
// ValidationError structure
interface ValidationError {
path: string; // Field path like 'user.email'
message: string; // Human-readable error message
code: string; // Error code like 'required' or 'min_length'
}
// Handling validation errors
const result = validator.validate(invalidData);
if (!result.isValid()) {
// Access all errors
result.errors.forEach(error => {
console.log(`${error.path}: ${error.message} (${error.code})`);
});
// Use tapError for side effects
result.tapError(errors => {
logValidationErrors(errors);
});
// Or throw exception
try {
result.unwrap(); // Throws LuqValidationException
} catch (e) {
if (e.name === 'LuqValidationException') {
console.log('Validation failed:', e.message);
console.log('Errors:', e.errors);
}
}
}
Functional Methods
The Result object supports functional programming patterns for composing and transforming results:
// Transform valid data
const transformed = result
.map(data => ({
...data,
timestamp: Date.now()
}))
.map(data => normalizeData(data));
// Chain validations
const finalResult = validator1.validate(data)
.flatMap(validData => validator2.validate(validData))
.flatMap(validData => validator3.validate(validData));
// Side effects without changing the result
const result = validator.validate(data)
.tap(validData => {
console.log('Valid:', validData);
saveToDatabase(validData);
})
.tapError(errors => {
console.error('Invalid:', errors);
logErrors(errors);
});
// Convert to plain object (for JSON serialization)
const plain = result.toPlainObject();
// { valid: boolean, data?: T, errors: ValidationError[] }
Validation Options
Both validate()
and parse()
accept an optional second parameter for configuration:
// ValidationOptions interface
interface ValidationOptions {
abortEarly?: boolean; // Stop at first error (default: false)
context?: any; // Additional context for validation
}
// Using validation options
const result = validator.validate(data, {
abortEarly: true, // Stop at first error
context: {
user: currentUser,
timestamp: Date.now()
}
});
// Options are passed to all field validators
// Useful for conditional validation based on context
Complete Examples
Form Validation with Transformations
// Form validation example
import { Builder } from '@maroonedog/luq/core';
import {
requiredPlugin,
stringMinPlugin,
stringEmailPlugin,
transformPlugin,
compareFieldPlugin
} from '@maroonedog/luq/plugin';
type RegistrationForm = {
username: string;
email: string;
password: string;
confirmPassword: string;
age: string; // From form input
};
const registrationValidator = Builder()
.use(requiredPlugin)
.use(stringMinPlugin)
.use(stringEmailPlugin)
.use(transformPlugin)
.use(compareFieldPlugin)
.for<RegistrationForm>()
.v('username', b => b.string.required().min(3))
.v('email', b => b.string.required().email())
.v('password', b => b.string.required().min(8))
.v('confirmPassword', b => b.string.required().compareField('password'))
.v('age', b => b.string.required().transform(v => parseInt(v, 10)))
.build();
// Handle form submission
function handleSubmit(formData: RegistrationForm) {
const result = registrationValidator.parse(formData);
return result
.map(data => {
// age is now a number
return createUser(data);
})
.tap(user => {
console.log('User created:', user.id);
})
.tapError(errors => {
displayFormErrors(errors);
});
}
API Endpoint Validation
// API validation with error response
import { Result } from '@maroonedog/luq';
async function updateUserEndpoint(req: Request): Promise<Response> {
const body = await req.json();
const result = userUpdateValidator.validate(body);
// Early return for validation errors
if (!result.isValid()) {
return new Response(JSON.stringify({
success: false,
errors: result.errors.map(e => ({
field: e.path,
message: e.message,
code: e.code
}))
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Process valid data
const userData = result.unwrap();
const updated = await updateUser(userData);
return new Response(JSON.stringify({
success: true,
data: updated
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
Composing Multiple Validators
// Composing multiple validators
const addressValidator = Builder()
.use(requiredPlugin)
.for<Address>()
.v('street', b => b.string.required())
.v('city', b => b.string.required())
.v('zipCode', b => b.string.required())
.build();
const userValidator = Builder()
.use(requiredPlugin)
.for<User>()
.v('name', b => b.string.required())
.v('email', b => b.string.required())
.build();
// Compose validators
function validateComplete(data: any): Result<CompleteData> {
const userResult = userValidator.validate(data.user);
const addressResult = addressValidator.validate(data.address);
if (!userResult.isValid()) return userResult;
if (!addressResult.isValid()) return addressResult;
return Result.ok({
user: userResult.unwrap(),
address: addressResult.unwrap()
});
}
API Reference
Validator Methods
Method | Returns | Description |
---|---|---|
validate(value, options?) | Result<T> | Validates and returns original values |
parse(value, options?) | Result<T> | Validates and returns transformed values |
pick(fieldName) | FieldValidator<T, F> | Extracts single field validator |
Result Methods
Method | Returns | Description |
---|---|---|
isValid() | boolean | Check if validation passed |
isError() | boolean | Check if validation failed |
unwrap() | T | Get data or throw exception |
unwrapOr(default) | T | Get data or return default |
unwrapOrElse(fn) | T | Get data or compute default |
map(fn) | Result<U> | Transform valid data |
flatMap(fn) | Result<U> | Chain result-returning functions |
tap(fn) | Result<T> | Side effect for valid data |
tapError(fn) | Result<T> | Side effect for errors |
data() | T | undefined | Get data or undefined |
errors | ValidationError[] | Get validation errors |
toPlainObject() | object | Convert to plain object |
FieldValidator Methods (from pick())
Method | Returns | Description |
---|---|---|
validate(value, allValues?, options?) | ValidationResult | Validates single field value |
• value | Field type | The field value to validate |
• allValues | Partial<T> | Context with other field values |