@janus-validator/core
v0.6.0
Published
A combinator library for defining validators that validate objects and provide examples
Maintainers
Readme
@janus-validator/core
Core validation library for Janus Validator - composable validators that can both validate data and generate valid examples. Perfect for testing, API validation, and form handling.
Features
- 🎯 Type-safe validation with clear, helpful error messages
- 🧬 Structured, recursive errors (per-field / per-index) with generated examples
- 🎲 Automatic data generation from validator definitions (great for tests/fixtures)
- 🧩 Composable combinators for complex schemas
- 🎭 Realistic presets for names, emails, addresses, etc.
- 🔧 Custom generators to override default generation
- 📝 Optional concise DSL available in @janus-validator/dsl
Installation
# Core + DSL package (recommended)
npm install @janus-validator/core @janus-validator/dsl
# Or just core (no DSL)
npm install @janus-validator/coreQuick Start
This demonstrates usage without the DSL, although usable, it's generally recommended you use the DSL. They are functionally equivalent, just the syntax is shorter and cleaner.
import { Generator } from '@janus-validator/core';
import {
Struct,
UnicodeString,
Integer,
Boolean,
Regex,
} from '@janus-validator/core/combinators';
// Define a validator (core API)
const userValidator = Struct({
name: UnicodeString(1, 100),
age: Integer(0, 150),
email: Regex(/^[\w.]+@[\w.]+\.\w+$/),
active: Boolean(),
});
// Validate data
const result = userValidator.validate({
name: 'Alice',
age: 30,
email: '[email protected]',
active: true,
});
if (result.valid) {
console.log('Valid:', result.value);
} else {
console.log('Error:', result.error);
console.log('Example:', result.example); // Auto-generated valid example
}
// Generate test data
const generator = new Generator({ random: Math.random });
const testUser = generator.generate(userValidator.domain);
// { name: 'Alice', age: 42, email: '[email protected]', active: true }The “two faces”: validate + generate
Janus validators can be used in both directions:
- Forwards:
validator.validate(unknown)→ValidationResult<T> - Backwards:
generator.generate(validator.domain)→T
This enables “round-trip” testing: anything generated by a validator should validate.
import { Generator } from '@janus-validator/core';
import { Struct, UnicodeString, Integer } from '@janus-validator/core/combinators';
const User = Struct({ name: UnicodeString(1, 50), age: Integer(0, 150) });
const generator = new Generator({ random: Math.random });
const sample = generator.generate(User.domain);
const result = User.validate(sample);
// result.valid === trueStructured errors (with examples)
When validation fails, you get:
error: a path-based summary (e.g.profile.age: Value 999 is greater than maximum 150)results: a recursive structure showing which parts passed/failedexample: an auto-generated valid value for the failing validator
import { Struct, UnicodeString, Integer } from '@janus-validator/core/combinators';
const Profile = Struct({ name: UnicodeString(1, 50), age: Integer(0, 150) });
const result = Profile.validate({ name: 'Alice', age: 999 });
if (!result.valid) {
result.error;
result.results; // per-field ValidationResult
result.example; // generated valid Profile
}DSL
The DSL lives in a separate package: @janus-validator/dsl.
It provides short aliases (O, U, I, B, Or, Seq, …) plus primitive/enum auto-wrapping.
Realistic Data Presets
Generate realistic test data:
import {
FirstName, LastName, FullName,
RealisticEmail, CorporateEmailPreset,
RealisticUSPhone,
RealisticStreetAddress, RealisticCity, RealisticState, RealisticZipCode,
CompanyName, ProductName,
RecentDate, FutureDate,
RealisticPrice,
} from '@janus-validator/core/lib';
import { O } from '@janus-validator/dsl';
import { Generator } from '@janus-validator/core';
const generator = new Generator({ random: Math.random });
const customer = O({
name: FullName(), // "Alice Smith"
email: RealisticEmail(), // "[email protected]"
phone: RealisticUSPhone(), // "(555) 123-4567"
address: O({
street: RealisticStreetAddress(), // "1234 Oak St"
city: RealisticCity(), // "New York"
state: RealisticState(), // "NY"
zip: RealisticZipCode(), // "10001"
}),
});
const testCustomer = generator.generate(customer.domain);Custom Generators
Override default generation with custom logic:
import { fromValues, templateGenerator, withGenerator } from '@janus-validator/core/combinators';
import { U, I, R } from '@janus-validator/dsl';
// Generate from a fixed list
const country = fromValues(R(/^[A-Z]{2}$/), ['US', 'CA', 'GB', 'DE', 'FR']);
// Template-based generation
const sku = templateGenerator(U(10, 20), (pick, rng) => {
const categories = ['ELEC', 'FURN', 'CLTH'];
const num = Math.floor(rng.random() * 10000).toString().padStart(4, '0');
return `${pick(categories)}-${num}`;
});
// Generates: "ELEC-0042", "FURN-1337", etc.
// Custom generation function
const userId = withGenerator(I(1, 1000000), (rng) => {
return Math.floor(rng.random() * 1000000) + 1;
});Error Messages with Examples
All DSL validators automatically include valid examples in error messages:
import { I } from '@janus-validator/dsl';
const age = I(18, 100);
const result = age.validate('not a number');
// result = {
// valid: false,
// error: 'Expected number, got string',
// example: 42 // Auto-generated valid example
// }Real-World Examples
API Request Validation
import { O, U, I, R, Or, Null } from '@janus-validator/dsl';
const createUserRequest = O({
username: R(/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/),
email: R(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/),
password: U(8, 128),
profile: O({
firstName: U(1, 50),
lastName: U(1, 50),
age: Or(I(13, 150), Null()),
}),
});
// In your API handler
const result = createUserRequest.validate(req.body);
if (!result.valid) {
return res.status(400).json({ error: result.error });
}Form Validation
import { O, U, R, createCaptureGroup } from '@janus-validator/dsl';
const { capture, ref, context } = createCaptureGroup();
const signupForm = O({
email: R(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/),
password: capture('pwd', U(8, 100)),
confirmPassword: ref('pwd'),
acceptTerms: true, // Must be true (auto-wrapped)
});
function validateForm(data: unknown) {
context.clear();
return signupForm.validate(data);
}E-Commerce Order
import { O, U, I, N, R, Or, oneOrMore } from '@janus-validator/dsl';
const orderItem = O({
productId: U(10, 50),
name: U(1, 200),
quantity: I(1, 100),
unitPrice: N(0.01, 100000),
});
const order = O({
orderId: R(/^ORD-\d{8}$/),
customerId: I(1, 1000000),
items: oneOrMore(orderItem),
subtotal: N(0, 1000000),
tax: N(0, 100000),
total: N(0, 1100000),
status: Or('pending', 'processing', 'shipped', 'delivered'), // Auto-wrapped!
createdAt: R(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/),
});Test Data Generation
import { Generator } from '@janus-validator/core';
const rng = { random: Math.random };
const generator = new Generator(rng);
// Generate 100 test users
const testUsers = Array.from({ length: 100 }, () =>
generator.generate(userValidator.domain)
);
// Use in tests
describe('UserService', () => {
it('should create users', () => {
const user = generator.generate(userValidator.domain);
const created = userService.create(user);
expect(created.id).toBeDefined();
});
});API Reference
Validator Interface
interface Validator<T> {
validate(input: unknown): ValidationResult<T>;
domain: Domain<T>;
}
type ValidationResult<T> =
| { valid: true; value: T }
| {
valid: false;
error: string;
example?: T;
results?: { [key: string]: ValidationResult<any> } | ValidationResult<any>[];
};Type Utilities
Helper types for working with validators:
import {
InferValidatorType, // Extract T from Validator<T>
UnionOfValidators, // [Validator<A>, Validator<B>] => A | B
TupleOfValidators, // [Validator<A>, Validator<B>] => [A, B]
ValidatorSchema, // { [key: string]: Validator<any> }
InferSchemaType, // { a: Validator<A> } => { a: A }
} from '@janus-validator/core';
// Example: Infer type from any validator
type UserType = InferValidatorType<typeof userValidator>;Generator Class
class Generator {
constructor(rng: RNG);
generate<T>(validator: Validator<T>): T;
}
interface RNG {
random(): number; // Returns 0-1
}License
MIT
