Schema Basics
FieldDefinition reference
interface FieldDefinition {
// ─── Required ─────────────────────────────────────────────────────────────
type:
| 'text'
| 'number'
| 'email'
| 'select'
| 'checkbox'
| 'radio'
| 'textarea'
| 'date'
| 'array'
| 'group'
| 'file'
/** Flat dot-path key in the values object, e.g. "address.city" */
name: string
// ─── Display ──────────────────────────────────────────────────────────────
label?: string
placeholder?: string
// ─── Initial value ────────────────────────────────────────────────────────
/** Static value or a function called at init with already-resolved partial values */
defaultValue?: unknown | ((values: Record<string, unknown>) => unknown)
// ─── Constraints ──────────────────────────────────────────────────────────
required?: boolean
disabled?: boolean | ((values: Record<string, unknown>) => boolean)
/** Boolean, function, or string expression evaluated against live values */
visible?: boolean | string | ((values: Record<string, unknown>) => boolean)
// ─── Validation ───────────────────────────────────────────────────────────
validators?: ValidatorFn[]
asyncValidators?: AsyncValidatorFn[]
// ─── Masking ──────────────────────────────────────────────────────────────
mask?: string | MaskConfig
// ─── select / radio options ───────────────────────────────────────────────
/** Static array, sync function, or async function */
options?:
| FieldOption[]
| ((values: Record<string, unknown>) => FieldOption[])
| ((values: Record<string, unknown>) => Promise<FieldOption[]>)
/** Field names that trigger async options re-fetch when their values change */
optionsDeps?: string[]
// ─── group / array ────────────────────────────────────────────────────────
fields?: FieldDefinition[]
// ─── transform / parse ────────────────────────────────────────────────────
/** Applied on every setField call — use for trim, coercion, formatting */
transform?: (value: unknown, values: Record<string, unknown>) => unknown
/** Applied at submit time to produce the final payload value */
parse?: (raw: unknown) => unknown
// ─── Custom component ─────────────────────────────────────────────────────
/** Vue component or registered name; receives FormFieldProps */
component?: Component | string
// ─── File field options ───────────────────────────────────────────────────
accept?: string // passed to <input accept>
multiple?: boolean
maxSize?: number // bytes (informational; use fileSize validator to enforce)
maxFiles?: number // informational; use fileCount validator to enforce
}Transform & parse
const schema: FieldDefinition[] = [
{
type: 'text',
name: 'username',
// trim on every keystroke
transform: (value) => (typeof value === 'string' ? value.trim() : value),
},
{
type: 'text',
name: 'tags',
defaultValue: 'vue,react',
// split into array at submit time — values.tags is still a string
parse: (raw) =>
String(raw)
.split(',')
.map((s) => s.trim()),
},
]transform mutates values immediately, on every setField call. parse runs only at submit time, to produce the final payload, and does not mutate values.
useForm composable
import { useForm } from '@macrulez/vue-form-schema'
const form = useForm(config)Config
schema
FieldDefinition[] | JSONSchema
Field definitions.
initialValues
Partial<T> · default: {}
Seed values (override field defaults).
validateOn
'input' | 'blur' | 'submit' | 'eager' · default: 'blur'
When validation fires — see validateOn: 'eager' below.
validateMode
'first' | 'all' · default: 'first'
Return first error only, or all errors — see Multiple errors per field.
clearOnHide
boolean · default: false
Reset field value when it becomes hidden.
onSubmit
(values: T) => void | Promise<void>
Called after successful validation.
persist
false | 'session' | 'local' · default: false
Persist values to sessionStorage / localStorage — see Persisted forms.
persistKey
string · default: auto
Storage key — defaults to the field names joined with , (not a hash); set it explicitly to avoid collisions between forms, see Persisted forms.
debug
boolean · default: false
Log state changes to console.group — see Debugging.
Return value
fields
ComputedRef<FieldDefinition[]>
Fields after conditions are evaluated.
values
Ref<T>
Current form values.
errors
Ref<Record<string, string[]>>
Validation errors keyed by field name.
touched
Ref<Record<string, boolean>>
Fields that have been blurred.
optionsLoading
Ref<Record<string, boolean>>
Async options loading state per field.
isDirty
ComputedRef<boolean>
true when values differ from initial state.
isValidating
ComputedRef<boolean>
true while any field's async validator is scheduled (debounced) or still in flight.
isValid
ComputedRef<boolean>
true when all visible fields pass validation and no async validator is still pending — reads false while isValidating is true.
isSubmitting
Ref<boolean>
true while onSubmit is running.
submit
() => Promise<void>
Touch all fields, validate, call onSubmit.
reset
(values?) => void
Restore initial state or supply new values.
setField
(path, value) => void
Set a value by dot-path.
getField
(path) => unknown
Read a value by dot-path.
validateOn: 'eager'
With 'eager', validation runs on input — but only after the field has been blurred at least once. This avoids showing errors while the user is still typing for the first time.
Schema formats
FieldDefinition array
import type { FieldDefinition } from '@macrulez/vue-form-schema'
const schema: FieldDefinition[] = [{ type: 'text', name: 'username', required: true }]
useForm({ schema })JSON schema
A serialisable format for server-driven schemas. Pass directly to useForm (auto-detected) or call parseJSON explicitly.
const raw = [
{
type: 'text',
name: 'username',
default: '',
required: true,
validators: [
{ rule: 'minLength', value: 3, message: 'At least 3 characters' },
{ rule: 'maxLength', value: 20 },
],
},
]
useForm({ schema: raw }) // auto-detected
// or
import { parseJSON } from '@macrulez/vue-form-schema'
const fields = parseJSON(raw)Supported JSON validator rules: required, minLength, maxLength, min, max, pattern, email, url. All accept an optional message override.
Zod
import { z } from 'zod'
import { parseZod } from '@macrulez/vue-form-schema/zod'
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.Zod → field type mapping: z.string() → text, z.number() → number, z.boolean() → checkbox, z.enum() → select, z.array() → array, z.object() → group. Use .describe('label') to set the field label.
parseZod also accepts a root z.discriminatedUnion(key, [...]) schema — see Discriminated schemas.
Yup
import { object, string, number } from 'yup'
import { parseYup } from '@macrulez/vue-form-schema/yup'
const schema = object({
name: string().required().label('Full name'),
email: string().email().required(),
age: number().min(0).optional(),
})
const fields = parseYup(schema)
const { values } = useForm({ schema: fields })
// values.value is typed from InferType<typeof schema> automaticallyValibot
import * as v from 'valibot'
import { parseValibot } from '@macrulez/vue-form-schema/valibot'
const schema = v.object({
name: v.pipe(v.string(), v.minLength(2)),
email: v.pipe(v.string(), v.email()),
age: v.optional(v.number()),
role: v.picklist(['admin', 'user']),
})
const fields = parseValibot(schema)
const { values } = useForm({ schema: fields })
// values.value is typed from v.InferOutput<typeof schema> automaticallyValibot → field type mapping: v.string() → text, v.number() → number, v.boolean() → checkbox, v.picklist() / v.enum() → select, v.array() → array, v.object() → group. v.pipe(v.string(), v.email()) → type: 'email'. v.optional() / v.nullable() → required: false.
parseValibot also accepts a root v.variant(key, [...]) schema — see Discriminated schemas.
OpenAPI / standard JSON Schema
Unlike parseJSON (this library's own simplified rule-based format), parseJSONSchema / parseOpenAPI accept real JSON Schema — the kind your backend already emits via OpenAPI/Swagger — so you don't need a translation layer between your API spec and the form.
import { parseOpenAPI } from '@macrulez/vue-form-schema/openapi'
// openapiDocument is your full OpenAPI document (e.g. fetched from /openapi.json)
const fields = parseOpenAPI(openapiDocument, { path: '/users', method: 'post' })
// or by JSON pointer into components.schemas:
const fields2 = parseOpenAPI(openapiDocument, '#/components/schemas/User')
const { values } = useForm({ schema: fields })Or on a standalone JSON Schema object, no OpenAPI wrapper:
import { parseJSONSchema } from '@macrulez/vue-form-schema/openapi'
const fields = parseJSONSchema({
type: 'object',
properties: {
name: { type: 'string', minLength: 2 },
age: { type: 'integer', minimum: 0 },
role: { type: 'string', enum: ['admin', 'user'] },
},
required: ['name', 'role'],
} as const)
const { values } = useForm({ schema: fields })
// values.value.role is typed 'admin' | 'user' — inferred from the `as const` schemaSupported subset: type (object / string / number / integer / boolean / array, including a type array like ['string', 'null']), properties + required, items (array item schemas — object items get their properties mapped to bare-named row fields, per array field conventions), enum / const → select, format (email, date / date-time, uri/url), minLength/maxLength/minimum/maximum/pattern, and local $refs (#/..., resolved against the document passed as rootDocument, or against the schema itself for self-contained $defs).
Not supported (deliberately — full JSON Schema is a lot of spec): oneOf / anyOf / allOf, additionalProperties, patternProperties, remote/external $ref, tuple-form items. Properties using these parse as a plain text field without the unsupported constraint, rather than throwing.
parseJSONSchema's return type carries a best-effort inferred value type from the schema literal (needs as const, same as defineSchema). parseOpenAPI can't infer statically (the extracted schema's shape depends on the path/selector argument at runtime) — pass an explicit type argument if you already generate one from your OpenAPI document, e.g. via openapi-typescript: parseOpenAPI<CreateUserRequest>(document, '#/components/schemas/User').