Skip to content

Schema Basics ​

FieldDefinition reference ​

ts
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 ​

ts
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 ​

ts
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 ​

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

ts
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 ​

ts
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 ​

ts
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> automatically

Valibot ​

ts
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> automatically

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

ts
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:

ts
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` schema

Supported 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').