Package Exports
- @molinit/validator
- @molinit/validator/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@molinit/validator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Validator
A flexible, type-safe validation library for TypeScript/JavaScript that provides declarative schema-based validation with support for complex data structures, conditional requirements, and custom validators.
Table of Contents
- Overview
- Features
- Installation
- Quick Start
- Core Concepts
- Validation Rules
- Advanced Usage
- API Reference
- Examples
- TypeScript Support
- Contributing
- License
Overview
Validator is designed to validate complex nested data structures using a declarative schema approach. Instead of writing imperative validation code, you define a schema that describes your data requirements, and the validator handles the rest.
Key Benefits
- Declarative: Define validation rules as data structures
- Type-safe: Full TypeScript support with strict typing
- Composable: Build complex validations from simple rules
- Extensible: Add custom validation functions
- Conditional: Support for dependent field validation
- Comprehensive: Validates primitives, objects, arrays, and nested structures
Features
- ✅ Primitive validation: strings, numbers, booleans
- ✅ Pattern matching: regex validation
- ✅ Length constraints: min/max length validation
- ✅ Enum validation: one-of allowed values
- ✅ Array validation: validate arrays and their items
- ✅ Object shape validation: validate object structures with type checking
- ✅ Array of objects: validate arrays of structured objects
- ✅ Conditional validation: fields required based on other fields
- ✅ Custom validators: add your own validation logic
- ✅ Comprehensive error messages: clear, actionable error reporting
Installation
npm install @molinit/validatorOr with yarn:
yarn add @molinit/validatorQuick Start
import validate from '@molinit/validator';
import { ValidationDescription } from '@molinit/validator/types';
// Define your validation schema
const userSchema: ValidationDescription = {
email: {
required: true,
regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
age: {
required: true,
minLength: 1,
maxLength: 3,
},
role: {
required: true,
oneof: ['admin', 'user', 'guest'],
},
};
// Validate data
const userData = {
email: 'user@example.com',
age: '25',
role: 'user',
};
const errors = validate(userData, userSchema);
if (errors.length > 0) {
console.error('Validation failed:', errors);
// [{ field: 'email', message: 'Missing required field' }]
} else {
console.log('Validation passed!');
}Core Concepts
Validation Schema
A validation schema is a plain JavaScript object that describes the validation rules for your data:
const schema: ValidationDescription = {
fieldName: {
// Validation rules go here
required: true,
minLength: 5,
maxLength: 100,
},
};Validation Rules
Each field in your schema can have multiple validation rules. All rules for a field must pass for the data to be valid.
Error Format
Validation errors are returned as an array of objects:
type ValidationError = {
field: string; // The field that failed validation
message: string; // Human-readable error message
};Validation Rules
required
Ensures a field is present and not empty.
{
username: {
required: true,
}
}Validates:
- Field exists
- Value is not
undefined,null, or empty string - For arrays: at least one item exists (when used with
array: true)
required_depends_on
Makes a field required only when another field has a specific value.
{
password_confirmation: {
required_depends_on: { key: 'change_password', value: '1' },
}
}Example:
// Valid: change_password is '0', so confirmation not required
{ change_password: '0', password_confirmation: '' }
// Invalid: change_password is '1', so confirmation is required
{ change_password: '1', password_confirmation: '' }minLength / maxLength
Validates the length of a string or the string representation of a value.
{
username: {
minLength: 3,
maxLength: 20,
}
}Example:
{
username: 'ab';
} // Invalid: too short
{
username: 'alice';
} // Valid
{
username: 'a'.repeat(21);
} // Invalid: too longregexp
Validates a field against a regular expression.
{
email: {
regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
zipCode: {
regexp: /^\d{5}(-\d{4})?$/,
}
}Example:
{
email: 'user@example.com';
} // Valid
{
email: 'invalid-email';
} // Invalidequal
Validates that a field equals a specific value.
{
confirmation: {
equal: 'I agree to the terms',
}
}oneof
Validates that a field is one of the allowed values (enum validation).
{
status: {
oneof: ['draft', 'published', 'archived'],
},
country: {
oneof: ['US', 'CA', 'MX', 'UK', 'FR', 'DE'],
}
}Example:
{
status: 'published';
} // Valid
{
status: 'pending';
} // Invalidarray
Validates that a field is an array.
{
tags: {
required: true,
array: true, // Must be an array with at least one item
}
}Example:
{
tags: ['javascript', 'typescript'];
} // Valid
{
tags: [];
} // Invalid: empty array with required: true
{
tags: 'string';
} // Invalid: not an arrayarray_items
Validates that all items in an array are from an allowed list.
{
tags: {
array: true,
array_items: ['javascript', 'typescript', 'python', 'rust'],
}
}Example:
{
tags: ['javascript', 'typescript'];
} // Valid
{
tags: ['javascript', 'php'];
} // Invalid: 'php' not in allowed listSupports:
- Primitives (strings, numbers, booleans)
- Objects (deep comparison via JSON)
shapeOf
Validates that a field is an object matching a specific shape with optional custom validators.
{
address: {
shapeOf: {
shape: {
street: 'string',
city: 'string',
zipCode: 'string',
country: ['US', 'CA', 'MX'], // Enum validation
},
required: ['street', 'city', 'country'],
validators: {
zipCode: (value) => /^\d{5}$/.test(value as string),
},
},
}
}Features:
- Type checking: Validates each property type
- Required properties: Specify which properties are mandatory
- Enum values: Use array for allowed values
- Custom validators: Add property-specific validation functions
Example:
// Valid
{
address: {
street: '123 Main St',
city: 'New York',
zipCode: '10001',
country: 'US',
}
}
// Invalid: missing required 'city'
{
address: {
street: '123 Main St',
country: 'US',
}
}
// Invalid: wrong type for 'zipCode'
{
address: {
street: '123 Main St',
city: 'New York',
zipCode: 12345, // Should be string
country: 'US',
}
}arrayOfShapes
Validates an array of objects, each matching a specific shape.
{
items: {
arrayOfShapes: {
shape: {
name: 'string',
quantity: 'number',
price: 'number',
category: ['electronics', 'clothing', 'food'],
},
required: ['name', 'quantity', 'price'],
validators: {
_array: (arr) => arr.length <= 100, // Array-level validator
price: (price) => price > 0, // Property-level validator
quantity: (qty) => qty > 0 && qty <= 1000,
},
},
}
}Features:
- Array-level validation:
_arrayvalidator for the entire array - Property-level validation: Individual validators for each property
- Type checking: Each object property validated
- Required properties: Per-object required fields
Example:
// Valid
{
items: [
{ name: 'Laptop', quantity: 2, price: 999.99, category: 'electronics' },
{ name: 'T-Shirt', quantity: 5, price: 19.99, category: 'clothing' },
];
}
// Invalid: second item has negative price
{
items: [
{ name: 'Laptop', quantity: 2, price: 999.99, category: 'electronics' },
{ name: 'T-Shirt', quantity: 5, price: -19.99, category: 'clothing' },
];
}at_least_one_of
Validates that at least one field from a group has a value. Useful when you have multiple optional fields but require at least one to be filled.
{
contact_email: {
at_least_one_of: {
fields: ['contact_email', 'contact_phone', 'contact_address'],
},
},
contact_phone: {
at_least_one_of: {
fields: ['contact_email', 'contact_phone', 'contact_address'],
},
},
contact_address: {
at_least_one_of: {
fields: ['contact_email', 'contact_phone', 'contact_address'],
},
},
}Features:
- Group validation: Checks if ANY field in the group has a value
- Conditional requirement: Optional
required_depends_onfor conditional validation - Flexible: Works with strings, numbers, booleans, and arrays
- Empty detection: Treats
'',null,undefined, and[]as empty
Example:
// Invalid: None of the contact fields have values
{
contact_email: '',
contact_phone: '',
contact_address: '',
}
// Error on all three fields: "At least one must be selected"
// Valid: At least one contact method provided
{
contact_email: 'user@example.com',
contact_phone: '',
contact_address: '',
}With Conditional Requirement:
{
social_facebook: {
at_least_one_of: {
fields: ['social_facebook', 'social_twitter', 'social_instagram'],
required_depends_on: { key: 'has_social', value: 'yes' },
},
},
// Same config for social_twitter and social_instagram
}When has_social = 'yes', at least one social media field must have a value.
Advanced Usage
Conditional Validation
Create fields that are only required when certain conditions are met:
const checkoutSchema: ValidationDescription = {
shipping_method: {
required: true,
oneof: ['standard', 'express', 'pickup'],
},
shipping_address: {
required_depends_on: { key: 'shipping_method', value: 'standard' },
shapeOf: {
shape: {
street: 'string',
city: 'string',
state: 'string',
zip: 'string',
},
required: ['street', 'city', 'state', 'zip'],
},
},
pickup_location: {
required_depends_on: { key: 'shipping_method', value: 'pickup' },
oneof: ['store_1', 'store_2', 'store_3'],
},
};Nested Object Validation
Validate complex nested structures:
const orderSchema: ValidationDescription = {
customer: {
required: true,
shapeOf: {
shape: {
name: 'string',
email: 'string',
phone: 'string',
},
required: ['name', 'email'],
validators: {
email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email as string),
phone: (phone) => /^\d{10}$/.test(phone as string),
},
},
},
items: {
required: true,
arrayOfShapes: {
shape: {
product_id: 'string',
quantity: 'number',
price: 'number',
},
required: ['product_id', 'quantity', 'price'],
validators: {
_array: (items) => items.length > 0 && items.length <= 50,
quantity: (qty) => qty > 0 && qty <= 100,
price: (price) => price >= 0,
},
},
},
total: {
required: true,
regexp: /^\d+\.\d{2}$/,
},
};Custom Validators
Create reusable custom validation functions:
// Custom validators
const isValidUUID = (value: unknown): boolean => {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(value as string);
};
const isValidBarcode = (value: unknown): boolean => {
const code = value as string;
return /^\d{8,14}$/.test(code) && hasValidChecksum(code);
};
const isStrongPassword = (value: unknown): boolean => {
const password = value as string;
return (
password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password)
);
};
// Use in schema
const userSchema: ValidationDescription = {
id: {
required: true,
shapeOf: {
shape: { uuid: 'string' },
required: ['uuid'],
validators: { uuid: isValidUUID },
},
},
password: {
required: true,
shapeOf: {
shape: { value: 'string' },
required: ['value'],
validators: { value: isStrongPassword },
},
},
};Multiple Type Options
Validate fields that can be multiple types:
{
value: {
shapeOf: {
shape: {
data: ['string', 'number'], // Can be string OR number
},
required: ['data'],
},
}
}API Reference
Main Function
validate(input, schema)
Validates input data against a schema.
Parameters:
input: ValidationInput- The data to validate (object with string keys)schema: ValidationDescription- The validation schema
Returns:
ValidationError[]- Array of validation errors (empty if valid)
Example:
const errors = validate(userData, userSchema);
if (errors.length === 0) {
// Data is valid
}Validator Class
You can also use the Validator class directly for more control:
import Validator from '@molinit/validator/Validator';
const validator = new Validator();
// Validate individual fields
const error = validator.validate_required('email', '', true, {});
// Returns: { field: 'email', message: 'Missing required field' }
const error2 = validator.validate_regexp(
'email',
'invalid',
/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
{},
);
// Returns: { field: 'email', message: "Field doesn't match the required pattern" }Available Methods
validate_required(field, value, options, input)validate_required_depends_on(field, value, options, input)validate_minLength(field, value, options, input)validate_maxLength(field, value, options, input)validate_regexp(field, value, options, input)validate_equal(field, value, options, input)validate_oneof(field, value, options, input)validate_array(field, value, options, input)validate_array_items(field, value, options, input)validate_shapeOf(field, value, options, input)validate_arrayOfShapes(field, value, options, input)validate_at_least_one_of(field, value, options, input)
Examples
Basic User Registration
const registrationSchema: ValidationDescription = {
username: {
required: true,
minLength: 3,
maxLength: 20,
regexp: /^[a-zA-Z0-9_]+$/,
},
email: {
required: true,
regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
password: {
required: true,
minLength: 8,
},
terms_accepted: {
required: true,
equal: 'yes',
},
};
const userData = {
username: 'alice_2024',
email: 'alice@example.com',
password: 'SecurePass123!',
terms_accepted: 'yes',
};
const errors = validate(userData, registrationSchema);
// errors = [] (valid)E-commerce Product
const productSchema: ValidationDescription = {
sku: {
required: true,
regexp: /^[A-Z]{3}-\d{4}$/,
},
name: {
required: true,
minLength: 2,
maxLength: 100,
},
category: {
required: true,
oneof: ['electronics', 'clothing', 'food', 'books'],
},
price: {
required: true,
regexp: /^\d+\.\d{2}$/,
},
tags: {
array: true,
array_items: ['new', 'sale', 'featured', 'popular', 'clearance'],
},
inventory: {
required: true,
shapeOf: {
shape: {
quantity: 'number',
location: 'string',
restock_date: 'string',
},
required: ['quantity', 'location'],
validators: {
quantity: (qty) => (qty as number) >= 0,
},
},
},
};Wine Listing (Real-World Example)
const wineListing Schema: ValidationDescription = {
name: {
required: true,
minLength: 2,
maxLength: 255,
},
vintage: {
required: true,
regexp: /^\d{4}$/,
},
wine_type: {
required: true,
oneof: ['red', 'white', 'rose', 'sparkling', 'dessert'],
},
origin_country: {
required: true,
regexp: /^[A-Z]{2}$/, // ISO country codes
},
grapes: {
required: true,
arrayOfShapes: {
shape: {
name: 'string',
percentage: 'string',
},
required: ['name', 'percentage'],
validators: {
_array: (grapes) => {
// Percentages must sum to 100
const total = grapes.reduce((sum, g) => sum + parseInt(g.percentage), 0);
return total === 100;
},
percentage: (pct) => {
const num = parseInt(pct as string);
return num >= 0 && num <= 100;
},
},
},
},
barcode: {
required_depends_on: { key: 'require_barcode', value: '1' },
shapeOf: {
shape: {
code: 'string',
type: ['EAN8', 'EAN13', 'UPC', 'GTIN14', 'unknown'],
normalized_gtin: 'string',
},
validators: {
code: (code) => /^\d{8,14}$/.test(code as string),
},
required: ['code', 'type'],
},
},
};TypeScript Support
Full Type Definitions
import {
ValidationDescription,
ValidationAttributes,
ValidationError,
ValidationInput,
ShapeOfValidation,
ArrayOfShapesValidation,
} from '@molinit/validator/types';
// Define typed schemas
const schema: ValidationDescription = {
// Your schema here
};
// Type-safe error handling
const errors: ValidationError[] = validate(input, schema);
// Custom validators are type-safe
const customValidator = (value: unknown): boolean => {
// Your validation logic
return true;
};Type Inference
The validator provides excellent type inference for your schemas:
const schema = {
email: {
required: true,
regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
} satisfies ValidationDescription;
// TypeScript knows all available validation rulesBest Practices
1. Define Schemas as Constants
// ✅ Good: Reusable schema
export const USER_SCHEMA: ValidationDescription = {
email: { required: true, regexp: /.../ },
};
// ❌ Bad: Inline schema
const errors = validate(data, { email: { required: true } });2. Use Custom Validators for Complex Logic
// ✅ Good: Reusable, testable validator
const isValidCreditCard = (value: unknown): boolean => {
const num = value as string;
return luhnCheck(num) && num.length >= 13;
};
// ❌ Bad: Complex regex
regexp: /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|...)$/;3. Group Related Schemas
// schemas/user.ts
export const USER_SCHEMA = {
/* ... */
};
export const USER_PROFILE_SCHEMA = {
/* ... */
};
// schemas/product.ts
export const PRODUCT_SCHEMA = {
/* ... */
};
export const PRODUCT_VARIANT_SCHEMA = {
/* ... */
};4. Provide Clear Error Messages
The validator provides default error messages, but you can wrap it for custom messages:
const errors = validate(data, schema);
if (errors.length > 0) {
const customErrors = errors.map((err) => ({
...err,
message: getCustomMessage(err.field, err.message),
}));
}5. Test Your Schemas
describe('User Schema', () => {
it('should validate valid user data', () => {
const validUser = { email: 'test@example.com', age: '25' };
const errors = validate(validUser, USER_SCHEMA);
expect(errors).toHaveLength(0);
});
it('should reject invalid email', () => {
const invalidUser = { email: 'invalid', age: '25' };
const errors = validate(invalidUser, USER_SCHEMA);
expect(errors).toContainEqual({
field: 'email',
message: expect.stringContaining('pattern'),
});
});
});Common Patterns
Optional Fields with Constraints
{
bio: {
required: false,
minLength: 10, // If provided, must be at least 10 chars
maxLength: 500, // And at most 500 chars
}
}Mutually Exclusive Fields
{
email: {
required_depends_on: { key: 'contact_method', value: 'email' },
regexp: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
phone: {
required_depends_on: { key: 'contact_method', value: 'phone' },
regexp: /^\d{10}$/,
},
}Percentage Validation
const grapes = {
arrayOfShapes: {
shape: {
name: 'string',
percentage: 'number',
},
required: ['name', 'percentage'],
validators: {
_array: (items) => {
const total = items.reduce((sum, item) => sum + item.percentage, 0);
return total === 100;
},
percentage: (pct) => pct >= 0 && pct <= 100,
},
},
};Error Handling
Accessing Errors
const errors = validate(data, schema);
// Check if valid
if (errors.length === 0) {
console.log('Valid!');
}
// Get specific field errors
const emailErrors = errors.filter((e) => e.field === 'email');
// Group errors by field
const errorsByField = errors.reduce(
(acc, err) => {
if (!acc[err.field]) acc[err.field] = [];
acc[err.field].push(err.message);
return acc;
},
{} as Record<string, string[]>,
);Custom Error Handling
function validateWithCustomErrors(data: any, schema: ValidationDescription) {
const errors = validate(data, schema);
return errors.map((err) => ({
field: err.field,
message: err.message,
code: getErrorCode(err.message),
severity: getSeverity(err.field),
}));
}Performance Tips
- Reuse Validator Instance: The validator is stateless and can be reused
- Cache Schemas: Define schemas once as constants
- Lazy Validation: Only validate fields that changed
- Optimize Custom Validators: Keep custom validators fast and simple
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Development Setup
git clone https://github.com/molinit/validator
cd validator
npm install
npm testRunning Tests
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # With coverageLicense
MIT License - see LICENSE file for details.
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: support@molinit.com
Changelog
See CHANGELOG.md for version history and updates.
Made with ❤️ by the Molinit team