@k8ordo/form

Schema mapping

What each zod schema becomes as a control and its attributes, along with what an empty control submits, the checks the browser does not run, and the schemas it cannot take.

On this page

Control types

The type in input for each kind of schema:

  • z.string(): type="text"
  • z.email(), z.url(): type="email", type="url"
  • z.iso.date(), z.iso.time(): type="date", type="time"
  • z.iso.datetime({ local: true }): type="datetime-local"
  • z.coerce.number(): type="number"
  • z.boolean(), z.literal(true): type="checkbox"
  • z.stringbool(): type="checkbox", with the value a checked box submits
  • z.file(): type="file"
  • A string with input: "password" metadata: type="password"
  • z.enum([…]): no type; spread it onto a <select>
  • z.array(z.enum([…])): no type; one checkbox per option
  • Anything else (z.uuid(), z.coerce.date(), …): type="text"

Stacking a check such as .regex() on z.email() or z.iso.date() does not change the control type.

Constraints and attributes

Schema constraints become these attributes:

  • .min(), .max() on a string: minLength, maxLength
  • .regex(): pattern, when it is the only regex and has no flags
  • .min(), .max() on a number: min, max
  • .multipleOf(): that value as step. .int() alone gives step="1", and neither gives step="any"
  • .mime() on a file: accept
  • When the schema rejects what an empty control submits: required

What an empty control submits

This decides both whether required is emitted and what parseForm hands the schema.

  • A text control: ""
  • An unchecked z.boolean() checkbox: false
  • A number, a z.coerce.bigint(), a file or a choice: nothing (undefined)
  • An unchecked z.stringbool() box: nothing (undefined)
  • A checkbox group with nothing checked: []

Checks the browser does not run

These cannot be expressed as HTML attributes, so they are listed in dropped. The server still runs every one.

  • A .refine() on the whole schema
  • An exclusive bound on a float (.gt(), .lt())
  • A regex with flags, and several regexes on one string
  • A regex on a control that ignores pattern, such as type="date"
  • .mime() on a file, and its size bounds
  • .min() on a checkbox group; declare minChecked to run it in the browser too
  • Constraints behind a .transform() or a z.custom(), which cannot be read
Warning

Pitfall

A .refine() on a single field, a nested object or a row is not listed in dropped yet. The browser lets it through without a word, and only the server checks it.

Schemas it refuses

formFields and parseForm throw, with the reason, on these:

  • z.number(), z.bigint(), z.date(), z.literal(1): every value arrives as a string, so nothing could pass. Use z.coerce
  • z.record, tuples, nullable objects, a repeat inside a repeat: there is no single name to submit under
  • Keys holding . or brackets: they cannot be told apart from the path separators
  • z.stringbool().default(true): unchecking the box could never submit false

What it does not do yet

  • Several files in one control. z.array(z.file()) derives repeated single-file rows
  • Rules on fields inside repeated rows
  • Input masking. Rewriting the value works, but managing the caret is a separate problem from wiring a form
k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2