Skip to content

Form Schema

v0.2.8Forms & ValidationVueNuxt

Reactive forms from a declarative schema (JSON, Zod, Yup, or Valibot) for Vue 3 — a headless, SSR-compatible alternative to VeeValidate/FormKit.

Form Schema
Get started →
npm install @macrulez/vue-form-schema@latest
01 — Purpose

When you'd reach for this

The backend already describes a form's shape in JSON Schema, OpenAPI, or a Zod type — vue-form-schema turns that description straight into a working form on screen, instead of manually duplicating the same fields and validation rules in a Vue component.

Some fields appear depending on others

Picking "business" reveals a tax ID field, and "individual" reveals passport details, and the two shouldn't show at once — the form decides on its own which fields to show and validate based on what's already been chosen.

A form needs a list with a variable number of rows

A list of phone numbers, invoice line items, or team members — the user adds and removes rows freely, and every new row gets validated and cleared the same way the first one was.

A long form is split into several steps

A ten-screen questionnaire is intimidating shown all at once — the form is broken into steps with its own validation for each, and the next step only opens once the previous one checks out.

The server rejects a form for a reason the client never checked

An email being already taken can only be discovered after submitting to the server. The response maps onto the right form fields in one call, so the email field itself turns red, not a generic "something went wrong" banner at the top.

02 — Features

At a glance

Any schema source — from JSON to Zod

Any schema source — from JSON to Zod

Build forms from FieldDefinition[], JSON, standard JSON Schema/OpenAPI, Zod, Yup, or Valibot. Adapters for each library are separate entry points, not bloating the main bundle. Type inference (InferValues) works automatically for all sources, giving full type safety.

Conditional fields and dynamic options

Conditional fields and dynamic options

Control field visibility and disabled state via booleans, reactive functions, or string expressions. Select/radio options can be sync or async with automatic refetch when dependencies change (optionsDeps). Everything updates reactively without unnecessary re‑renders.

Validation and input masking

Validation and input masking

Built‑in sync and async validators (required, minLength, email, sameAs, fileType, and more) with validateOn modes (input/blur/submit/eager) and validateMode (first/all). Input masks with presets (phone, date, IBAN, INN) and custom patterns are applied automatically.

Dynamic arrays and multi-step forms

Dynamic arrays and multi-step forms

useFieldArray gives full control over dynamic lists: append, prepend, remove, move, swap, replace. useMultiStepForm manages wizards with per‑step validation and a shared submit handler. Ready‑to‑use renderers (ArrayField, MultiStepFormRenderer) speed up development.

Headless, UI themes, and SSR safety

Headless, UI themes, and SSR safety

The core is UI‑agnostic — use any design via custom components or a registry. Ready‑to‑use renderers for Tailwind, shadcn/ui, PrimeVue, and Naive UI. SSR safety, persistence (local/sessionStorage), a DevTools inspector, and a Nuxt module with auto‑imports.

Schema composition and server-side errors

Schema composition and server-side errors

Compose large forms from reusable pieces with mergeSchemas, omitFields, pickFields, and extendField — without duplicating field definitions across forms. discriminatedFields swaps in a different set of fields by another field’s value, and applyServerErrors maps backend errors onto the right fields in one call.

03 — Quick example

See how it works

A schema straight from Zod — typed out of the box

parseZod() turns an ordinary Zod schema into form fields — value types come from z.infer<>, no useForm<Values>() needed.

zod-schema.ts
import { z } from 'zod'
import { parseZod } from '@macrulez/vue-form-schema/zod'
import { useForm } from '@macrulez/vue-form-schema'

const schema = z.object({
  name: z.string().min(2).describe('Full name'),
  age: z.number().min(0).optional(),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
})

const fields = parseZod(schema)
const { values } = useForm({ schema: fields })

// values.value.name is string, values.value.age is number | undefined, ...
// — inferred automatically from `schema` via z.infer<typeof schema>, no
// useForm<Values>(...) needed.

Fields that show and hide themselves

visible takes a function of the current values — the field only renders and validates while the condition holds, and clearOnHide resets it once hidden.

conditional-fields.ts
import type { FieldDefinition } from '@macrulez/vue-form-schema'
import { useForm } from '@macrulez/vue-form-schema'

const schema: FieldDefinition[] = [
  { type: 'checkbox', name: 'hasCompany', label: 'I represent a company' },
  {
    type: 'text',
    name: 'companyName',
    label: 'Company name',
    visible: (values) => values['hasCompany'] === true,
    required: true,
  },
]

const { values } = useForm({ schema, clearOnHide: true })

Backend errors land on the right fields automatically

applyServerErrors parses a Laravel/DRF response (or any shape via a custom mapper) and maps errors onto the right fields on its own — no hand-rolled response unwrapping per backend.

server-errors.ts
import { applyServerErrors } from '@macrulez/vue-form-schema'

const res = await fetch('/api/users', { method: 'POST', body: JSON.stringify(form.values.value) })

if (!res.ok) {
  const { formErrors } = applyServerErrors(form, await res.json(), { format: 'laravel' })
  if (formErrors.length) toast.error(formErrors[0]) // errors not tied to a field
}

// Built-in formats: 'laravel', 'drf', 'flat' — or pass your own mapper.
// The next client-side validation naturally replaces a stale server error.