Dynz is a headless schema library that replaces scattered form logic with a single TypeScript definition any consumer can evaluate — a Frontend, a Backend, an LLM, or an external system. Conditions, validation rules, and derived values are expressed as data, not code, making schemas portable, testable, and channel-agnostic by design.
- Headless by design — no UI components, no rendering opinions. Bring your own renderer and wire it to the schema.
- Serializable expression system — conditions, validation rules, and derived values are data structures, not JavaScript functions. Schemas survive a JSON round-trip and can be evaluated in any runtime.
- Cross-field expressions — d.ref(), d.multiply(), d.add() and friends compose freely inside any rule slot, making inter-field dependencies first-class rather than bolted on via refine.
- Runtime schema generation — schemas are plain TypeScript objects, designed to be generated by a NestJS service at request time based on user context, product state, or business rules.
- Universal consumer support — the same schema drives a web UI, a mobile app, an AI agent via MCP, or a third-party integration without modification.
npm install dynz
# or
pnpm add dynz
# or
yarn add dynzimport * as d from "dynz";
// Define a schema
const userSchema = d.object({
name: d.string().min(2),
email: d.string().email(),
password: d.string().min(6),
passwordConfirm: d.string().equals(d.ref("password")),
});
// Send it to the external system (e.g. frontend)
return Response.json(userSchema);
// Get the response from the request
const schema = await response.json();
// Validate the response
const result = d.validate(schema, undefined, {
name: "John Doe",
email: "john@example.com",
password: "MyPassword",
passwordConfirm: "MyPassword",
});
if (result.success) {
console.log("Valid data:", result.values);
} else {
console.log("Validation errors:", result.errors);
}dynz supports comprehensive schema types for all common data structures:
import * as d from "dynz";
// String schema with validation rules
const nameSchema = d.string().min(2).max(5).regex("^[a-zA-Z\\s]+$");
// Number schema with constraints
const ageSchema = d.number().min(0).max(120);
// Object schema with nested fields
const userSchema = d.object({
profile: d.object({
name: d.string().min(1),
bio: d.string().optional(),
}),
});
// Array schema
const tagsSchema = d.array(d.string().min(1)).min(5).max(20);Extensive validation rules for precise data validation:
import * as d from "dynz";
const productSchema = d.object({
name: d.string().min(1).max(100),
email: d.string().email(),
price: d.number().min(0),
category: d.options(["electronics", "books", "clothing"]),
sku: d.string().regex("^[A-Z]{3}-\\d{4}$"),
quantity: d.string().isNumeric(),
});dynz excels at dynamic validation based on other field values:
import * as d from "dynz";
const userSchema = d.object({
accountType: d.options(["personal", "business"]),
industry: d.options(["finance", "healthcare", "tech"]),
// Included only for business accounts
companyName: d
.string()
.min(2)
.setIncluded(d.eq(d.ref("accountType"), "business")),
// Different validation rules based on account type
email: d
.string()
.email()
.when(d.eq(d.ref("accountType"), "business"), (b) =>
b.regex(
"@company\\.com$",
undefined,
"Business accounts must use company email",
),
),
// Complex conditional logic
specialField: d
.string()
.setRequired(
d.and(
d.eq(d.ref("accountType"), "business"),
d.or(
d.eq(d.ref("industry"), "finance"),
d.eq(d.ref("industry"), "healthcare"),
),
),
),
});Reference other fields in validation rules:
import * as d from "dynz";
const signupSchema = d.object({
password: d.string().min(8),
confirmPassword: d.string().equals(d.ref("password"), "Passwords must match"),
birthYear: d
.date()
.after(new Date(new Date().setFullYear(new Date().getFullYear() - 18))),
graduatedAt: d
.date()
.after(d.ref("birthYear"), "Graduation date must be after birth year"),
});Control when fields can be modified based on conditions:
import * as d from "dynz";
function buildSchema(user: { role: "admin" | "user" }) {
return d.object({
status: d.options(["draft", "published"]),
// mutability as a constant, but dynamically generated
title: d
.string()
.min(1)
.setMutable(user.role === "admin"),
// mutability as a predicate; resolved in the schema itself
content: d.string().setMutable(d.eq(d.ref("status"), "draft")),
// mutability as a constant
createdAt: d.string().setMutable(false),
});
}You can also control mutability on inner schemas of an array. This allows you to add new elements or remove elements, but not to mutate elements on the same index.
import * as d from "dynz";
// The array is mutable, but each element is immutable
const schema = d.array(d.string().setMutable(false));
d.validate(schema, [], ["foo"]); // Validates successfully, because it's a new entry
d.validate(schema, ["foo"], []); // Validates successfully, because an entry is removed
d.validate(schema, ["foo"], ["bar"]); // Returns an error since 'foo' is mutated into 'bar'Dynamically include or exclude fields:
import * as d from "dynz";
const registrationSchema = d.object({
dietryRestrictions: d.boolean(),
dietryDetail: d.string().setIncluded(d.eq(d.ref("dietryRestrictions"), true)),
});Create reusable custom validation logic:
import * as d from "dynz";
const passwordStrengthRule = d.custom("passwordStrength", {
minScore: 4,
requireSpecialChars: true,
});
const passwordStrengthRuleValidator: d.CustomRuleFunction = (value, params) => {
// ... validation logic
};
// Use custom() to add arbitrary rules to a fluent chain
const strongPasswordSchema = d
.string()
.min(8)
.custom("passwordStrength", { minScore: 4, requireSpecialChars: true });
d.validate(strongPasswordSchema, undefined, "myStrongPassword", {
customRules: {
passwordStrength: passwordStrengthRuleValidator,
},
});import * as d from "dynz";
const orderSchema = d.object({
orderType: d.options(["standard", "express", "international"]),
shippingMethod: d
.options(["overnight", "same-day", "air", "sea"])
.when(d.eq(d.ref("orderType"), "express"), (b) =>
b.oneOf(["overnight", "same-day"]),
)
.when(d.eq(d.ref("orderType"), "international"), (b) =>
b.oneOf(["air", "sea"]),
)
.setRequired(
d.or(
d.eq(d.ref("orderType"), "express"),
d.eq(d.ref("orderType"), "international"),
),
),
customsInfo: d
.object({
value: d.number().min(0),
description: d.string().min(1),
})
.setIncluded(d.eq(d.ref("orderType"), "international")),
});import * as d from "dynz";
// Order management with status-based mutability
const orderSchema = d.object({
orderStatus: d.options(["draft", "pending", "send"]).setMutable(false), // always immutable
items: d
.array(d.string())
.setMutable(
d.or(
d.eq(d.ref("orderStatus"), "draft"),
d.eq(d.ref("orderStatus"), "pending"),
),
),
shippingAddress: d
.string()
.setMutable(
d.or(
d.eq(d.ref("orderStatus"), "draft"),
d.eq(d.ref("orderStatus"), "pending"),
),
),
});string()- String validation schema with fluent methodsnumber()- Number validation schema with fluent methodsboolean()- Boolean validation schema with fluent methodsobject(fields)- Object schema with nested field schemasarray(schema)- Array schema with item validationdate()- Date validation schema with fluent methodsoptions(values)- Enum-like validation for predefined valuesfile()- File validation schema with fluent methods
String schemas:
.min(value, code?)- Minimum string length.max(value, code?)- Maximum string length.email(code?)- Email format validation.regex(pattern, flags?, code?)- Regular expression validation.equals(value, code?)- Exact value matching.oneOf(values, code?)- Must be one of specified values.isNumeric(code?)- Numeric string validation
Number schemas:
.min(value, code?)- Minimum numeric value.max(value, code?)- Maximum numeric value.maxPrecision(value, code?)- Maximum decimal precision
Date schemas:
.before(value, code?)- Date must be before value.after(value, code?)- Date must be after value.minDate(value, code?)- Minimum date (inclusive).maxDate(value, code?)- Maximum date (inclusive)
Array schemas:
.min(value, code?)- Minimum array length.max(value, code?)- Maximum array length
.setRequired(value)- Mark as required (boolean or predicate).optional()- Shorthand for.setRequired(false).setMutable(value)- Control field mutability (boolean or predicate).setIncluded(value)- Control field inclusion (boolean or predicate).setPrivate(value)- Mark as private/masked.setCoerce(value)- Enable automatic type coercion.setDefault(value)- Set default value
.when(predicate, callback)- Apply rules conditionally
Predicates return a boolean and are used wherever a schema accepts a conditional expression: .setRequired(), .setMutable(), .setIncluded(), and .when().
Comparison:
eq(left, right)- Equals (===)neq(left, right)- Not equals (!==)gt(left, right)- Greater than (>)gte(left, right)- Greater than or equal (>=)lt(left, right)- Less than (<)lte(left, right)- Less than or equal (<=)matches(value, pattern, flags?)- Regex pattern matching
Collection:
isIn(value, array)- Value is contained in arrayisNotIn(value, array)- Value is not contained in array
Logical combinators:
and(...predicates)- All predicates must be trueor(...predicates)- At least one predicate must be true
Transformers compute a value and can be used as an input to any predicate or validation rule.
Arithmetic:
sum(...values)- Add values togethersub(...values)- Subtract values left to rightmultiply(...values)- Multiply values togetherdivide(...values)- Divide values left to rightmin(...values)- Smallest of the given valuesmax(...values)- Largest of the given values
Math:
ceil(value)- Round up to nearest integerfloor(value)- Round down to nearest integersin(value)- Sinecos(value)- Cosinetan(value)- Tangent
Utility:
age(dateValue)- Age in years derived from a date valuesize(value)- Length of a string or array, size in bytes of a filelookup(value, table)- Map a value through a lookup table
ref(path)- Reference another field's valuev(value)- Wrap a static/constant value
validate(schema, currentValues?, newValues, options?)- Main validation functionvalidateMutableoption - Check field mutability constraints (defaults to true)customRulesoption - Provide custom rule implementations
dynz provides excellent TypeScript integration:
import * as d from "dynz";
const schema = d.object({
name: d.string().min(1),
age: d.number().optional(),
tags: d.array(d.string()),
});
// Inferred type: { name: string; age?: number; tags: string[] }
type UserData = d.SchemaValues<typeof schema>;
// Type-safe validation results
const result = d.validate(schema, undefined, {
name: "John",
tags: ["dynz"],
});
if (result.success) {
// result.values is properly typed as UserData
console.log(result.values.name); // ✅ Type-safe access
}Check out the /examples directory for complete working examples:
- Next.js Example - React forms with dynz schemas
import * as d from "dynz";
const loanApplicationSchema = d.object({
applicantType: d.options(["individual", "business"]),
income: d
.number()
.min(0)
.setIncluded(d.eq(d.ref("applicantType"), "individual")),
businessRevenue: d
.number()
.min(0)
.setIncluded(d.eq(d.ref("applicantType"), "business")),
loanAmount: d
.number()
.min(1000)
// Can't exceed annual income for individuals
.when(d.eq(d.ref("applicantType"), "individual"), (b) =>
b.max(d.ref("income")),
)
// Can't exceed annual revenue for businesses
.when(d.eq(d.ref("applicantType"), "business"), (b) =>
b.max(d.ref("businessRevenue")),
),
});import * as d from "dynz";
const documentWorkflowSchema = d.object({
content: d
.string()
.setMutable(
d.and(
d.eq(d.ref("status"), "draft"),
d.or(
d.eq(d.ref("isOwner"), true),
d.eq(d.ref("hasEditPermission"), true),
),
),
),
status: d
.options(["draft", "review", "approved", "published"])
.setMutable(
d.or(
d.and(
d.eq(d.ref("currentStatus"), "draft"),
d.eq(d.ref("isOwner"), true),
),
d.and(
d.eq(d.ref("currentStatus"), "review"),
d.eq(d.ref("userRole"), "reviewer"),
),
d.eq(d.ref("userRole"), "admin"),
),
),
});We welcome contributions! Please see our Contributing Guide for details.
MIT © dynz
- More framework integrations coming soon...
Built with ❤️ for type-safe validation