Skip to content

Conditional rules: .when() / .sometimes() (unblocks stacksjs/stacks#1890) #1107

Description

@glennmichael123

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

  • .when(field, value | predicate, fn) and .sometimes() on the field builder
  • Both roundtrip through schema introspection (oneOf / allOf in the JSON-schema-style serialization)
  • Conditional failures surface with field-specific error messages
  • Tests covering cross-field conditional + sometimes-present + nested-object conditional

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions