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

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.
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 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 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.
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.

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.

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.

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.

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.

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.

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.
parseZod() turns an ordinary Zod schema into form fields — value types come from z.infer<>, no useForm<Values>() needed.
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.visible takes a function of the current values — the field only renders and validates while the condition holds, and clearOnHide resets it once hidden.
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 })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.
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.