Dynz.

Custom rules

Attach named validation rules to any schema field, then provide implementations at validation time.

Built-in rules cover the common cases, but real applications often need checks that can't be expressed with a static predicate — an async uniqueness check, a profanity filter, a barcode checksum, a call to an external service.

Custom rules bridge the gap: you declare the rule name (and any parameters) in the schema, then provide the implementation when you call validate().

Declaring a custom rule

Use .custom(name, params?) in any fluent chain:

import * as d from 'dynz';

const schema = d.object({
  username: d.string().min(3).custom('unique-username'),
  password: d.string().min(8).custom('password-strength', { minScore: 3 }),
  barcode:  d.string().custom('ean13-checksum'),
});

The rule name and params become part of the schema object — they travel with it over JSON serialization. The implementation lives separately, injected at validation time.

Providing implementations

Pass customRules in the validation options:

const result = await d.validate(schema, undefined, formData, {
  customRules: {
    'unique-username': async (value, params) => {
      const exists = await db.users.exists({ username: value });
      if (exists) return false; // or return { message: 'Username is taken' }
    },

    'password-strength': async (value, params) => {
      const score = zxcvbn(value).score;
      return score >= params.minScore;
    },

    'ean13-checksum': (value) => {
      return isValidEAN13(value);
    },
  },
});

Implementation contract

A custom rule function receives (value, params) and should:

  • Return undefined or true — validation passes
  • Return false — validation fails with a generic error
  • Return { message: string } — validation fails with a specific message
  • Be async if it needs to call a database or external service
type CustomRuleFunction = (
  value: unknown,
  params: Record<string, unknown>,
) => boolean | { message: string } | undefined | Promise<boolean | { message: string } | undefined>;

Custom error codes

Every rule — including custom ones — accepts an optional code as the last argument. The code becomes error.customCode in the failure result:

d.string().custom('unique-username', undefined, 'username_taken')
// error.customCode === 'username_taken'

This lets your UI map specific error codes to specific messages without parsing the message string.

Sharing rule implementations

For reuse across schemas, define implementations as named constants:

// lib/validation-rules.ts
export const sharedCustomRules = {
  'unique-email': async (value: unknown) => {
    const exists = await db.users.exists({ email: value });
    return !exists;
  },

  'password-strength': async (value: unknown, params: { minScore?: number }) => {
    const score = zxcvbn(String(value)).score;
    return score >= (params.minScore ?? 2);
  },
} satisfies Record<string, CustomRuleFunction>;

// In your handler
await d.validate(schema, undefined, body, {
  customRules: sharedCustomRules,
});

Example: real-time form validation with custom rules

When using @dynz/react-hook-form, the resolver calls validate() under the hood. Pass schemaOptions to wire in your custom rule implementations:

const form = useDynzForm({
  schema,
  schemaOptions: {
    customRules: {
      'unique-username': async (value) => {
        const res = await fetch(`/api/check-username?q=${value}`);
        const { available } = await res.json();
        return available;
      },
    },
  },
});

The custom rule runs on every validation cycle (e.g. on blur or submit). Add debouncing in your rule implementation if you need to limit API calls.

Custom rules and serialization

Custom rule names and parameters are serialized with the schema — the name 'unique-username' and params { minScore: 3 } travel over JSON just like built-in rules.

The implementations are not serialized — they're injected per-environment. This is the intended design: the server declares what to check, and each consumer (frontend, backend, agent) provides the appropriate implementation for its context.

On this page