Dynz.

Conditions

Make fields required, included, or mutable based on the values of other fields — using predicates that are data, not code.

The three conditional properties — required, included, and mutable — each accept either a static boolean or a predicate. A predicate is a serializable expression that evaluates to true or false at validation time.

This means your conditions travel with the schema. The server can generate a schema with conditions and send it to the client as JSON — the client evaluates it without knowing what the conditions are ahead of time.

Required

A field is required by default. Mark it optional with .optional(), or make it conditionally required with .setRequired(predicate):

import * as d from 'dynz';

const schema = d.object({
  // Always required (default)
  name: d.string().min(1),

  // Never required
  bio: d.string().optional(),

  // Required only when 'accountType' is 'business'
  companyName: d.string().min(1)
    .setRequired(d.eq(d.ref('accountType'), 'business')),

  accountType: d.options(['personal', 'business']),
});

When required evaluates to false and the field has no value, validation passes and result.values will have undefined at that key.

Included

included controls whether a field is part of the interaction at all. An excluded field must have no value — if a value is submitted for an excluded field, validation fails.

const schema = d.object({
  hasCompany: d.boolean(),

  // This field only exists when 'hasCompany' is true
  companyName: d.string().min(1)
    .setIncluded(d.eq(d.ref('hasCompany'), true)),
});

The difference between required: false and included: false:

required: falseincluded: false
Field shown in UIYesNo
Value can be submittedYes (optional)No (error if present)
Appears in outputYes (as undefined)No

If your form might submit stale values for fields that have since become excluded (e.g. the user switched hasCompany back to false but the field value is still in the payload), pass stripNotIncludedValues: true to silently remove them instead of failing:

const result = await d.validate(schema, undefined, {
  hasCompany:  false,
  companyName: 'Acme', // excluded — would normally be an error
}, { stripNotIncludedValues: true });

// result.success === true
// result.values.companyName === undefined

Mutable

mutable controls whether a field can change value between an update. It's enforced when you pass currentValues as the second argument to validate().

const schema = d.object({
  status: d.options(['draft', 'submitted', 'approved']),

  // Title can only change while in draft
  title: d.string().min(1)
    .setMutable(d.eq(d.ref('status'), 'draft')),

  // Created date is always immutable
  createdAt: d.string().setMutable(false),

  // Items are editable until submitted
  items: d.array(d.string())
    .setMutable(
      d.or(
        d.eq(d.ref('status'), 'draft'),
        d.eq(d.ref('status'), 'submitted'),
      )
    ),
});

// On create — no mutability enforcement (currentValues is undefined)
await d.validate(schema, undefined, newData);

// On update — mutability rules apply
await d.validate(schema, existingRecord, updatedData);

When mutable is false and the value has changed from currentValues, validation returns an error with code immutable.

Predicates

All three properties accept any predicate. Predicates compose freely:

Comparison predicates

d.eq(d.ref('status'), 'active')     // field === 'active'
d.neq(d.ref('role'), 'guest')       // field !== 'guest'
d.gt(d.ref('age'), 17)              // field > 17
d.gte(d.ref('score'), 60)           // field >= 60
d.lt(d.ref('quantity'), 100)        // field < 100
d.lte(d.ref('price'), d.ref('budget'))  // price <= budget
d.matches(d.ref('email'), '@company\\.com$')  // regex match

Collection predicates

d.isIn(d.ref('country'), ['US', 'CA', 'MX'])
d.isNotIn(d.ref('plan'), ['free', 'trial'])

Logical combinators

// AND — all conditions must be true
d.and(
  d.eq(d.ref('accountType'), 'business'),
  d.eq(d.ref('verified'), true),
)

// OR — at least one must be true
d.or(
  d.eq(d.ref('role'), 'admin'),
  d.eq(d.ref('role'), 'editor'),
)

// Combine freely
d.and(
  d.eq(d.ref('status'), 'active'),
  d.or(
    d.eq(d.ref('role'), 'admin'),
    d.gte(d.ref('planLevel'), 2),
  )
)

Real-world example: Loan application

const loanSchema = d.object({
  applicantType: d.options(['individual', 'business']),

  // Individual fields — only included for personal applications
  annualIncome: d.number().min(0)
    .setIncluded(d.eq(d.ref('applicantType'), 'individual')),

  employmentStatus: d.options(['employed', 'self-employed', 'retired'])
    .setIncluded(d.eq(d.ref('applicantType'), 'individual')),

  // Business fields — only included for business applications
  businessRevenue: d.number().min(0)
    .setIncluded(d.eq(d.ref('applicantType'), 'business')),

  yearsInBusiness: d.number().min(0)
    .setIncluded(d.eq(d.ref('applicantType'), 'business')),

  // Loan amount is capped differently per type
  loanAmount: d.number().min(1000)
    .when(d.eq(d.ref('applicantType'), 'individual'), (b) =>
      b.max(d.multiply(d.ref('annualIncome'), 5))
    )
    .when(d.eq(d.ref('applicantType'), 'business'), (b) =>
      b.max(d.multiply(d.ref('businessRevenue'), 2))
    ),

  // Can only change the amount while in draft
  status: d.options(['draft', 'submitted', 'approved']).setMutable(false),
});

Checking conditions at runtime (React)

If you're using @dynz/react-hook-form, the hooks useIsRequired, useIsIncluded, and useIsMutable resolve conditions reactively as form values change. See the React Hook Form guide.

On this page