Expressions
Reference other fields, compute derived values, and compose transformers — all as serializable data.
Expressions are the building blocks that connect fields to each other. They appear wherever a validation rule or condition needs a dynamic value — instead of a hardcoded number, you pass an expression that resolves at runtime against the current form state.
Because expressions are plain objects, they survive serialization: you can send them from a server, store them in a database, and evaluate them in any environment.
d.ref(path)
References the value of another field by dot-separated path.
import * as d from 'dynz';
const schema = d.object({
password: d.string().min(8),
confirmPassword: d.string().equals(d.ref('password')),
startDate: d.date(),
endDate: d.date().after(d.ref('startDate'), 'End must be after start'),
});For nested fields, use dot notation:
const schema = d.object({
address: d.object({
country: d.string(),
}),
vatNumber: d.string()
.setRequired(d.eq(d.ref('address.country'), 'DE')),
});d.v(value) — static value wrapper
Wraps a static value so it can be passed in expression position. Rarely needed directly, but useful when building dynamic schemas programmatically.
d.eq(d.ref('plan'), d.v('pro'))
// equivalent to
d.eq(d.ref('plan'), 'pro')Arithmetic transformers
Compute numbers from other field values or static numbers:
d.sum(d.ref('basePrice'), d.ref('taxAmount')) // addition
d.sub(d.ref('total'), d.ref('discount')) // subtraction
d.multiply(d.ref('quantity'), d.ref('unitPrice')) // multiplication
d.divide(d.ref('totalCost'), d.ref('quantity')) // division
d.min(d.ref('requested'), d.ref('available')) // smaller of two
d.max(d.ref('price'), d.v(0)) // larger of two (floor at 0)Nest them freely:
// Tax-inclusive total: (price + tax) * quantity
d.multiply(
d.sum(d.ref('unitPrice'), d.ref('taxPerUnit')),
d.ref('quantity')
)Math transformers
d.ceil(d.ref('value')) // Math.ceil
d.floor(d.ref('value')) // Math.floor
d.sin(d.ref('angle'))
d.cos(d.ref('angle'))
d.tan(d.ref('angle'))
d.atan(d.ref('angle'))Utility transformers
d.age(dateValue)
Computes age in whole years from a date value. Useful for age-gated rules:
const schema = d.object({
birthDate: d.date().before(new Date()),
// Driver's license — must be at least 16
driversLicense: d.string()
.setRequired(d.gte(d.age(d.ref('birthDate')), 16)),
// Alcohol — must be at least 21
alcoholPurchase: d.boolean()
.setIncluded(d.gte(d.age(d.ref('birthDate')), 21)),
});d.size(value)
Returns the length of a string or array, or the size in bytes of a File.
// Require at least 3 comma-separated items
d.string().setRequired(d.gte(d.size(d.ref('tags')), 3))d.lookup(value, table)
Maps a value through a lookup table (a record of value → result):
const taxRates = { US: 0.08, GB: 0.2, DE: 0.19 };
const schema = d.object({
country: d.options(['US', 'GB', 'DE']),
price: d.number().min(0),
taxRate: d.expression(d.lookup(d.ref('country'), taxRates)),
total: d.expression(
d.multiply(d.ref('price'), d.sum(d.v(1), d.ref('taxRate')))
),
});d.pluck(value, key)
Extracts a property from an object value.
d.pluck(d.ref('selectedItem'), 'price')Using expressions in d.expression() schemas
The d.expression() schema type creates a computed, read-only field. It doesn't accept user input — its value is always derived from other fields.
const orderSchema = d.object({
unitPrice: d.number().min(0),
quantity: d.number().min(1),
subtotal: d.expression(d.multiply(d.ref('unitPrice'), d.ref('quantity'))),
tax: d.expression(d.multiply(d.ref('subtotal'), d.v(0.2))),
total: d.expression(d.sum(d.ref('subtotal'), d.ref('tax'))),
});
const result = await d.validate(orderSchema, undefined, {
unitPrice: 49.99,
quantity: 3,
});
if (result.success) {
console.log(result.values.subtotal); // 149.97
console.log(result.values.tax); // 29.994
console.log(result.values.total); // 179.964
}Using expressions in rules
Any rule parameter can be an expression. This lets rules depend on the live state of other fields:
const schema = d.object({
budget: d.number().min(0),
price: d.number().min(0).max(d.ref('budget')), // can't exceed budget
startYear: d.number().min(2000),
endYear: d.number().min(d.ref('startYear')), // must be >= startYear
minLength: d.number().min(1),
text: d.string().min(d.ref('minLength')), // dynamic minimum
});How expressions serialize
A reference like d.ref('password') is just this object:
{ "type": "ref", "path": "password" }And d.eq(d.ref('plan'), 'enterprise') becomes:
{
"type": "eq",
"left": { "type": "ref", "path": "plan" },
"right": "enterprise"
}This is why you can JSON.stringify a dynz schema, send it over HTTP, and reconstruct it with JSON.parse — all the logic is already data.