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 |