Skip to content

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

@glennmichael123

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions