dynz
v0.0.19
Published
A powerful TypeScript schema validation with advanced conditional logic, cross-field validation, and static type inference.
Maintainers
Readme
dynz
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.
Features
- 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.
Installation
npm install dynz
# or
pnpm add dynz
# or
yarn add dynzQuick Start
import * 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: "[email protected]",
password: "MyPassword",
passwordConfirm: "MyPassword",
});
if (result.success) {
console.log("Valid data:", result.values);
} else {
console.log("Validation errors:", result.errors);
}Core Concepts
Schema Types
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);Validation Rules
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(),
});Conditional Logic
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"),
),
),
),
});Cross-Field References
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"),
});Mutability Controls
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),
});
}Mutability on array schemas
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'Field Inclusion
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)),
});Advanced Usage
Custom Rules
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,
},
});Complex Conditional Schemas
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")),
});Mutable Conditions in Practice
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"),
),
),
});API Reference
Schema Builders
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
Fluent Rule 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
Property Setters (all schema types)
.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
Conditional Rules
.when(predicate, callback)- Apply rules conditionally
Predicates
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
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
Helpers
ref(path)- Reference another field's valuev(value)- Wrap a static/constant value
Validation
validate(schema, currentValues?, newValues, options?)- Main validation functionvalidateMutableoption - Check field mutability constraints (defaults to true)customRulesoption - Provide custom rule implementations
Type Safety
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
}Examples
Check out the /examples directory for complete working examples:
- Next.js Example - React forms with dynz schemas
Advanced Features
Complex Business Logic
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")),
),
});Workflow Management
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"),
),
),
});Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
MIT © dynz
Related Packages
- More framework integrations coming soon...
Built with ❤️ for type-safe validation
