HomeDocsComponentsBlocksForm contractValidation
Form contract / Zod companions

Zod companions

A matching schema per component, exported from a separate entry point so Zod never enters your component bundle.

Separate entry point, on purpose

// The component. No Zod anywhere in this import graph.
import { PhoneInput } from "@/components/ui/phone-input"

// The schema. Only pulled in where you actually validate.
import { phoneSchema } from "@inputcn/phone/schema"

const schema = z.object({
  phone: phoneSchema({ mobileOnly: true, countries: ["GB"] }),
})

Formatting options go in both places

A schema validates a value, so it never sees the props you gave the component. That is usually invisible — until a message has to format a bound. moneySchema defaults to USD, so a GBP field whose cap is enforced by the resolver reports “Must be $1,000.00 or less” unless you tell the schema too.

Pass currency and locale to both. The same applies to DurationInput’s units and FileSizeInput’s binary flag: anything that only affects how a number is printed has to be given to whichever side is printing it.

The same options on both sides

const schema = z.object({
  budget: moneySchema({
    positive: true,
    max: 1_000_000,
    currency: "GBP",   // message formatting only — defaults to USD
    locale: "en-GB",
  }),
})

<CurrencyInput label="Budget" currency="GBP" locale="en-GB" {...field} />

The schema and the field agree

Each schema takes the same constraint object the component takes, and a parity test asserts that a value the component accepts is a value the schema accepts, and the reverse. A resolver that disagrees with the field it is validating is worse than no resolver at all, because the form refuses to submit and the field claims everything is fine.

z.infer of a companion schema is assignable to the component’s value prop, which is checked at type level rather than left to documentation.

The schemas

ENTRY POINTEXPORTVALIDATES
@inputcn/phone/schemaphoneSchemaE.164 string
@inputcn/currency/schemamoneySchemainteger minor units
@inputcn/masked/schemamaskedSchemaraw string against the mask
@inputcn/percent/schemapercentSchemafraction
@inputcn/card/schemacardSchemadigits, Luhn and brand
@inputcn/duration/schemadurationSchemaseconds
@inputcn/color/schemacolorSchemahex
@inputcn/cron/schemacronSchemaexpression
@inputcn/filesize/schemafileSizeSchemabytes
@inputcn/ip/schemacidrSchemaaddress or CIDR
@inputcn/mention/schemamentionSchema{ text, ids }

Zod 4 is what the suite runs against today. A dual-version run against Zod 3 is planned and not yet wired up.