Pliant

A modern, framework-agnostic validation engine with typed rules, inheritance, and structured errors.

🎯 Named Rules 🧬 Rule Inheritance 📦 Structured Errors 💬 Message Resolvers 📋 Rulesets 🌐 Browser + Node
$ npm install @pliant/core

Live Validation Playground

Try all the built-in rules in action. Each field demonstrates a different validation rule.

Uses required rule – validates non-empty values
Uses email rule – validates email format
Uses inherited length rule with custom bounds
Uses range rule – min/max numeric bounds
Uses min rule – minimum value only
Uses max rule – maximum value only
Uses numeric rule – digits only
Uses regex rule – custom pattern
Uses minLength rule
Uses equals rule – cross-field validation
Setup Code

              
💡 How it works

Rules 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.

Live Error Output (JSON)
// errors appear here...

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 none Validates value is not empty, null, or undefined required
email none Validates standard email format email
length { min?, max? } Validates string length within bounds length
minLength { min } Validates minimum string length (inherits from length) length
maxLength { max } Validates maximum string length (inherits from length) length
numeric none Validates value contains only digits numeric
range { min?, max?, inclusive? } Validates numeric value within range range
min { min, inclusive? } Validates minimum numeric value min
max { max, inclusive? } Validates maximum numeric value max
regex { pattern: RegExp } Validates against custom regex pattern regex
equals { field?, value?, strict? } Validates equality with another field or value equals
Importing Rules
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.

🧬
Extend Any Rule

Use inheritRule() to create a new rule based on an existing one with different defaults.

⚙️
Override Options

Change min/max bounds, patterns, or any other options while keeping the same validation logic.

💬
Custom Messages

Each inherited rule can have its own default message, perfect for domain-specific feedback.

Creating Inherited Rules
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"]);
📌 Why Inheritance?

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.

Defining a Ruleset
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: {...} }
📋 Field Rules

Define an array of rules for each field. Rules are evaluated in order, and all failing rules return errors.

🔗 Group Rules

Group rules validate the entire data object. Use them for cross-field validation, async checks, or business logic.

📦 Structured Output

Ruleset errors are neatly organized by field and group, making it easy to display errors in your UI.

💡 Pro Tip

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.

Creating a Message Resolver
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" } }
🔤 Static Messages

Simple string messages for straightforward validations. Great for required fields or format checks.

⚡ Dynamic Messages

Functions receive the error detail and context. Build messages with actual values, field names, or custom logic.

🌍 i18n Ready

Message resolvers are perfect for internationalization. Swap catalogs based on locale, or use a translation function inside.

📝 Message Priority

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

TypeScript 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[];
}