Dynz.

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.

On this page