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: false | included: false | |
|---|---|---|
| Field shown in UI | Yes | No |
| Value can be submitted | Yes (optional) | No (error if present) |
| Appears in output | Yes (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 === undefinedMutable
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 matchCollection 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.