Background
ts-validation has no way to express "this rule only applies under condition X." The two patterns this blocks:
// (1) "required only when another field is X"
schema.object({
payment_method: schema.enum(['card', 'bank']),
card_token: schema.string().when('payment_method', 'card', s => s.required()),
bank_account: schema.string().when('payment_method', 'bank', s => s.required()),
})
// (2) "rule only when the field is present at all"
schema.object({
username: schema.string().sometimes().minLength(3), // skip when key absent
})
Today apps work around this with custom refine() callbacks, but:
- Error messages are generic ("validation failed") instead of pointing at the conditional field
- Schema introspection can't reflect the conditional structure — OpenAPI exporters / form-renderers see a flat "required" or "optional" rather than the real shape
This blocks stacksjs/stacks#1890 (the Stacks-side audit item that defers entirely to this upstream work).
Proposed API
// Cross-field conditional
field.when(otherField, value | predicate, refineFn)
// "Sometimes present"
field.sometimes()
Both must roundtrip through the schema-introspection API so downstream readers (OpenAPI exporter, form renderer, request-shape inference) can see the conditional structure rather than collapsed-to-required.
Out of scope (for the initial PR)
.requiredIfAccepted() / .prohibitedIf() and the long Laravel tail — add piecemeal as concrete demand surfaces
- Cross-form validation (wizard-state spanning multiple submissions) — that's a different abstraction
Acceptance
Background
ts-validation has no way to express "this rule only applies under condition X." The two patterns this blocks:
Today apps work around this with custom
refine()callbacks, but:This blocks stacksjs/stacks#1890 (the Stacks-side audit item that defers entirely to this upstream work).
Proposed API
Both must roundtrip through the schema-introspection API so downstream readers (OpenAPI exporter, form renderer, request-shape inference) can see the conditional structure rather than collapsed-to-required.
Out of scope (for the initial PR)
.requiredIfAccepted()/.prohibitedIf()and the long Laravel tail — add piecemeal as concrete demand surfacesAcceptance
.when(field, value | predicate, fn)and.sometimes()on the field builderoneOf/allOfin the JSON-schema-style serialization)