Live Validation Playground
Try all the built-in rules in action. Each field demonstrates a different validation rule.
required rule – validates non-empty valuesemail rule – validates email formatlength rule with custom boundsrange rule – min/max numeric boundsmin rule – minimum value onlymax rule – maximum value onlynumeric rule – digits onlyregex rule – custom patternminLength ruleequals rule – cross-field validationRules are registered in a central registry. When validating, you pass rule names (or refs with options) to evaluateRules(). Errors come back as structured objects with codes and details.
Built-in Rules Reference
Pliant ships with 11 battle-tested validation rules. Each returns a structured error object with a code and contextual details.
| Rule | Options | Description | Error Code |
|---|---|---|---|
| required | Validates value is not empty, null, or undefined | required |
|
| Validates standard email format | email |
||
| length | Validates string length within bounds | length |
|
| minLength | Validates minimum string length (inherits from length) | length |
|
| maxLength | Validates maximum string length (inherits from length) | length |
|
| numeric | Validates value contains only digits | numeric |
|
| range | Validates numeric value within range | range |
|
| min | Validates minimum numeric value | min |
|
| max | Validates maximum numeric value | max |
|
| regex | Validates against custom regex pattern | regex |
|
| equals | Validates equality with another field or value | equals |
import { requiredRule, emailRule, lengthRule, minLengthRule, maxLengthRule, numericRule, rangeRule, minRule, maxRule, regexRule, equalsRule } from "@pliant/core";
Rule Inheritance
Create specialized rules by inheriting from base rules. Override options, messages, or both without duplicating logic.
Use inheritRule() to create a new rule based on an existing one with different defaults.
Change min/max bounds, patterns, or any other options while keeping the same validation logic.
Each inherited rule can have its own default message, perfect for domain-specific feedback.
import { createRegistry, addRules, inheritRule, lengthRule, rangeRule } from "@pliant/core"; const registry = createRegistry(); addRules(registry, { // Base rules length: lengthRule({ min: 0, max: 255 }), range: rangeRule({ min: 0, max: 100 }), // Inherited rules with custom options username: inheritRule("length", { options: { min: 3, max: 20 }, message: "Username must be 3-20 characters" }), password: inheritRule("length", { options: { min: 8, max: 128 }, message: "Password must be at least 8 characters" }), age: inheritRule("range", { options: { min: 18, max: 120 }, message: (detail) => `Age must be between ${detail.min} and ${detail.max}` }), rating: inheritRule("range", { options: { min: 1, max: 5 }, message: "Rating must be 1-5 stars" }) }); // Now use these rules by name const errors = evaluateRules(registry, value, ctx, ["required", "username"]);
Instead of creating one-off validation functions everywhere, define meaningful rule names in your registry. This makes validation declarative, consistent, and easy to maintain across your entire application.
Rulesets – Validate Entire Forms
Rulesets let you define field-level and group-level validation in one declarative object. Perfect for form validation.
import { evaluateRuleset } from "@pliant/core"; const signupRuleset = { // Field-level rules fields: { email: ["required", "email"], username: ["required", "usernameLength"], password: ["required", "passwordLength"], confirm: [ "required", { name: "equals", options: { field: "password" } } ] }, // Group-level rules (run on entire data object) group: [ "termsAccepted", "uniqueEmail" ] }; // Evaluate all rules at once const formData = { email: "user@example.com", username: "cooluser", password: "secret123", confirm: "secret123" }; const errors = evaluateRuleset(registry, formData, signupRuleset); // Returns structured errors: // { fields: { email: {...}, password: {...} }, group: {...} }
Define an array of rules for each field. Rules are evaluated in order, and all failing rules return errors.
Group rules validate the entire data object. Use them for cross-field validation, async checks, or business logic.
Ruleset errors are neatly organized by field and group, making it easy to display errors in your UI.
Rulesets are plain objects – you can compose them, merge them, or generate them dynamically based on form state.
Message Resolvers
Transform error codes into human-readable messages. Use static strings or dynamic functions with access to error details.
import { createMessageResolver, applyMessages } from "@pliant/core"; // Create a resolver with static and dynamic messages const messages = createMessageResolver({ // Static string required: "This field is required", email: "Please enter a valid email address", // Dynamic function with error details length: (detail) => { if (detail.actual < detail.min) { return `Must be at least ${detail.min} characters`; } return `Must be no more than ${detail.max} characters`; }, range: (detail, ctx) => `${ctx.field} must be between ${detail.min} and ${detail.max}`, equals: (detail) => detail.field ? `Must match ${detail.field}` : "Values must match" }); // Apply messages to errors const errors = evaluateRules(registry, value, ctx, rules); const resolved = applyMessages(errors, ctx, messages); // Each error now has a `message` property // { required: { code: "required", message: "This field is required" } }
Simple string messages for straightforward validations. Great for required fields or format checks.
Functions receive the error detail and context. Build messages with actual values, field names, or custom logic.
Message resolvers are perfect for internationalization. Swap catalogs based on locale, or use a translation function inside.
Messages are resolved in this order: rule-level message → message resolver → error code. This lets you override messages at any level.
API Reference
Quick reference for all exports from @pliant/core.
Core Functions
| Function | Description |
|---|---|
| createRegistry() | Creates an empty rule registry |
| addRules(registry, rules) | Adds multiple rules to a registry |
| getRule(registry, name) | Retrieves a rule by name, resolving inheritance |
| evaluateRules(registry, value, ctx, rules) | Evaluates rules against a value, returns errors or null |
| evaluateRuleset(registry, data, ruleset) | Evaluates a ruleset against form data |
| inheritRule(base, overrides) | Creates an inherited rule definition |
| createMessageResolver(catalog) | Creates a message resolver from a catalog |
| applyMessages(errors, ctx, resolver) | Applies resolved messages to error objects |
Types
// Error detail returned by rules interface PliantErrorDetail { code: string; message?: string; [key: string]: unknown; } // Context passed to rules interface RuleContext<TData = Record<string, unknown>> { field?: string; data?: TData; } // Rule definition interface RuleDef<TOptions, TValue> { validate?: (value: TValue, ctx: RuleContext, options: TOptions) => RuleResult; validateAsync?: (value: TValue, ctx: RuleContext, options: TOptions) => Promise<RuleResult>; message?: string | MessageBuilder; inherit?: string; options?: TOptions; enabled?: boolean; meta?: Record<string, unknown>; } // Rule reference (string or object) type RuleRef = | string | { name: string; options?: Record<string, unknown>; message?: string }; // Ruleset for form validation interface Ruleset { fields?: Record<string, RuleRef[]>; group?: RuleRef[]; }