react-formbridge
Browse documentation
Validationv1.0.2

Schema validator bridge (zod, yup, joi, valibot)

Use the bridge option when your real validation source of truth already lives in Zod, Yup, Valibot, Joi, or another schema library.

  • The schema builders still drive rendering and UX metadata
  • The external schema owns the final values/errors decision
  • Successful parsed/coerced values are now forwarded to submit handlers
  • This is often the cleanest path in domains that already share validation with the backend
zod-bridge.tsts
1import { z } from 'zod'
2import { field, useFormBridge, zodBridge } from '@runilib/react-formbridge'
3
4const schema = {
5 email: field.email('Email').required(),
6 password: field.password('Password').required(),
7}
8
9const zodSchema = z.object({
10 email: z.string().email(),
11 password: z.string().min(8),
12})
13
14const form = useFormBridge(schema, {
15 validatorBridge: zodBridge(zodSchema),
16})

Shared customization options

All four built-in adapters share the same customization surface, so you can keep the same mental model even if the schema library changes later.

Shared adapter options (BridgeAdapterOptions) supported by all built-in bridges:

MethodTypeDescription
rootKey?string | nullWhere pathless errors land. Default '_root'. Set null to drop them
errorMode?'first' | 'join' | 'last'How duplicate field errors are aggregated
joinMessagesWith?stringSeparator for errorMode: 'join'
formatPath?(path, issue) => stringRewrite the final error key
mapIssue?(context) => Issue | nullRemap, skip, or rewrite an issue before it hits the error bag
normalizeMessage?(message, issue) => stringFinal message normalization hook
bridge-options.tsts
1import { field, joiBridge, useFormBridge } from '@runilib/react-formbridge'
2import Joi from 'joi'
3
4const schema = {
5 email: field.email('Email').required(),
6 plan: field.select('Plan').required(),
7}
8
9const joiSchema = Joi.object({
10 email: Joi.string().email({ tlds: false }).required(),
11 plan: Joi.string().required(),
12})
13
14const form = useFormBridge(schema, {
15 validatorBridge: joiBridge(joiSchema, {
16 rootKey: 'form',
17 errorMode: 'join',
18 joinMessagesWith: ' · ',
19 formatPath: (path) => path.map(String).join('.'),
20 normalizeMessage: (message) => message.trim(),
21 mapIssue: ({ defaultMessage, defaultPathKey }) => {
22 if (defaultPathKey === 'plan') {
23 return { message: `Billing: ${defaultMessage}` }
24 }
25
26 return undefined
27 },
28 }),
29})

Library-specific options

Each adapter also exposes the options you usually need from its schema engine.

Library-specific bridge options:

MethodTypeDescription
zodBridge(schema, { mode?, parseOptions?, ...shared })mode?: 'auto' | 'sync' | 'async' + Zod parseOptions
yupBridge(schema, { mode?, validateOptions?, ...shared })mode?: 'auto' | 'sync' | 'async' + Yup validateOptions
joiBridge(schema, { mode?, validateOptions?, stripQuotes?, ...shared })mode?: 'auto' | 'sync' | 'async' + Joi validateOptions + stripQuotes
valibotBridge(schema, { mode?, parseOptions?, module?, ...shared })mode?: 'auto' | 'sync' | 'async' + Valibot parseOptions + module

mode accepts 'auto', 'sync', or 'async'. The default 'auto' picks the async method when available, then falls back to sync. For Valibot, pass module: v if you want to avoid relying on runtime require(), especially in stricter ESM/browser setups.

Tips

TipDetails
Bridge contractMust return { values, errors } - the built-in adapters already handle this for you
Prefer built-insCustomize an existing adapter before writing a custom bridge from scratch
Root errorsDefault to '_root' - useful for banner-level or submit-level failures
Cross-platformA bridge works with the same useFormBridge() API on web and native
When to reach for a bridgeBusiness validation already exists elsewhere - otherwise prefer builder rules
Valibot specificsExpects valibot installed in the consumer app, or passed explicitly via module: v