Cross-Field Rules
Conditional requirements, mutual exclusion, field comparisons, and sum validation
Object schemas support rules that validate relationships between fields. These go beyond single-field constraints and express business logic like "if field A has value X, then field B is required."
Fluent Conditional API
The .when() method starts a fluent chain:
const schema = w.object({
type: w.enum(["individual", "business"] as const),
ssn: w.string().ssn().optional(),
ein: w.string().ein().optional(),
})
.when("type").equals("individual").require("ssn")
.when("type").equals("business").require("ein");Condition Methods
Start a condition chain with .when(field):
.when("field").equals(value) // Field equals a specific value
.when("field").in([v1, v2, v3]) // Field is one of several values
.when("field").exists() // Field is not null/undefined
.when("field").notExists() // Field is null/undefined
.when("field").notEquals(value) // Field does not equal a value
.when("field").gte(number) // Field >= number
.when("field").lte(number) // Field <= numberConsequent Methods
After the condition, specify what must be true:
.require("field") // Field must exist
.requireEquals("field", value) // Field must equal a value
.requireIn("field", [v1, v2]) // Field must be one of values
.requireMatch("field", /^\d+$/) // Field must match regex
.requirePredicate(predicate, code, message) // Custom predicate
.forbid("field") // Field must NOT existComposing Conditions
Chain conditions with .and() or .or():
schema
.when("country").equals("US").and("type").equals("individual").require("ssn")
.when("amount").gte(10000).or("risk").equals("high").require("verification");Helper Methods
requireWith
If field A exists, field B must also exist:
schema.requireWith("streetAddress", "city")
// If streetAddress is provided, city is requiredrequireWithout
If one field is absent, another field must exist:
schema.requireWithout("email", "phone")
// If phone is missing, email is requiredmutuallyExclusive
Two fields cannot both be present:
schema.mutuallyExclusive("ssn", "ein")
// Cannot have both SSN and EINrequireOneOf
At least one of the listed fields must exist:
schema.requireOneOf(["phone", "email", "fax"])
// At least one contact method requiredrequireExactlyOneOf
Exactly one of the listed fields must exist:
schema.requireExactlyOneOf(["ssn", "ein", "itin"])
// One identifier is required, but multiple identifiers are rejectedrequireFieldsMatch
Two fields must have equal values:
schema.requireFieldsMatch("password", "confirmPassword")
schema.requireFieldsMatch("scheduleB/total", "form941/liability")Field Comparison
Compare field values:
schema.requireFieldGreaterThan("endDate", "startDate")
schema.requireFieldGreaterOrEqual("maxAmount", "minAmount")
schema.requireFieldLessThan("discount", "subtotal")
schema.requireFieldLessOrEqual("used", "available")requireSum
Fields must sum to a target field:
schema.requireSum(["line1", "line2", "line3"], "total")
// line1 + line2 + line3 must equal totalrequireSumEquals
Fields must sum to a specific value:
schema.requireSumEquals(["percent1", "percent2", "percent3"], 100)
// Percentages must sum to 100Custom Rules
For predicates that don't fit the helpers, use .rule() directly:
import type { Predicate } from "wellformed-ts";
schema.rule(
{
type: "implies",
if: { type: "eq", path: "/status", value: "complete" },
then: { type: "exists", path: "/completedDate" },
},
"MISSING_DATE",
"Completed items must have a completion date"
)Custom Error Messages
All cross-field methods accept an options object:
schema.requireWith("streetAddress", "city", {
code: "MISSING_CITY",
message: "City is required when an address is provided",
help: "Please provide the city for the address",
})The options object supports code, message, help, source, and id.