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
}useForm composable
import { useForm } from '@macrulez/vue-form-schema'
const form = useForm(config)Config
| Property | Type | Default | Description |
|---|---|---|---|
schema | FieldDefinition[] | JSONSchema | — | Field definitions |
initialValues | Partial<T> | {} | Seed values (override field defaults) |
validateOn | 'input' | 'blur' | 'submit' | 'eager' | 'blur' | When validation fires |
validateMode | 'first' | 'all' | 'first' | Return first error only, or all errors |
clearOnHide | boolean | false | Reset field value when it becomes hidden |
onSubmit | (values: T) => void | Promise<void> | — | Called after successful validation |
persist | false | 'session' | 'local' | false | Persist values to sessionStorage / localStorage |
persistKey | string | auto | Storage key prefix |
debug | boolean | false | Log state changes to console.group |
Return value
| Property | Type | Description |
|---|---|---|
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 |
isValid | ComputedRef<boolean> | true when all visible fields pass validation |
isSubmitting | Ref<boolean> | true while onSubmit is running |
submit() | () => Promise<void> | Touch all fields, validate, call onSubmit |
reset(values?) | — | Restore initial state or supply new values |
setField(path, value) | — | Set a value by dot-path |
getField(path) | — | 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').