Skip to content

Reusable Validators

Validation logic is often repeated across many forms.

Typical examples are:

  • required text fields
  • numeric formatting
  • country-specific identifiers
  • file upload restrictions
  • grouped validation rules
  • workflow-dependent requirements

Without reusable validators, the same validation logic is often copied across multiple schemas.

Small inline validators are convenient at first.

validate(path.name, (ctx) => {
return ctx.value()?.trim()
? null
: { kind: 'required' };
});

As forms grow, however, inline validation logic tends to spread across the application.

Common problems are:

  • duplicated validation logic
  • inconsistent validation behavior
  • repeated error messages
  • difficult testing
  • poor discoverability
  • tightly coupled schemas

Reusable validators move validation logic into dedicated functions.

requiredTrimmed(path.name);

Instead of redefining the validation behavior every time, the validator becomes a reusable building block.

This improves:

  • consistency
  • readability
  • reuse
  • maintainability

Reusable validators also make schemas easier to read.

Instead of:

validate(path.price, (ctx) => {
// complex decimal validation
});

schemas can express intent directly:

decimal(path.price, {
maxIntegerDigits: 5,
maxFractionDigits: 2,
});

The schema describes what should happen instead of how the validation works internally.

Many validations are not generic technical rules.

They often represent domain concepts.

For example:

requiredDefined(path.acceptTerms);

This validator exists because standard required validation often treats false as invalid.

In many forms, however:

  • false is a valid value
  • but a selection is still mandatory

The validator captures that specific intent explicitly.

Reusable validators can encapsulate complex behavior.

mimeType(
path.attachment,
['image/*', 'application/pdf']
);

The consuming schema does not need to know:

  • MIME wildcard handling
  • file parsing
  • matching logic
  • reactive updates

The validator owns the implementation details.

Some validators focus on structural correctness instead of value ranges.

decimal(path.price, {
maxIntegerDigits: 5,
maxFractionDigits: 2,
});

This validates:

  • integer digit count
  • fraction digit count
  • localized decimal parsing

instead of only checking numeric min/max ranges.

Reusable validators are also useful for cross-field validation.

requiredAtLeastOne(
path,
['email', 'phone']
);

Instead of manually validating multiple fields repeatedly, the rule becomes reusable and self-documenting.

Reusable validators work especially well together with schema composition.

const ContactValidationSchema = schema<ContactForm>((path) => {
requiredTrimmed(path.name);
requiredTrimmed(path.email);
});

This schema fragment can then be reused across:

  • onboarding forms
  • edit forms
  • enterprise workflows
  • multi-step forms

A good rule of thumb is:

Extract validation logic once it represents a reusable concept.

Not every validation needs its own helper.

However, if validation logic:

  • appears repeatedly
  • contains domain meaning
  • requires custom error handling
  • contains structural complexity
  • is difficult to read inline

then a reusable validator usually improves maintainability.

Reusable validators help create schemas that are:

  • more readable
  • more declarative
  • easier to test
  • easier to compose
  • easier to reuse
  • easier to evolve over time

Instead of embedding validation details directly into schemas, validators become modular building blocks for form architecture.