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
}

useForm composable

ts
import { useForm } from '@macrulez/vue-form-schema'
const form = useForm(config)

Config

PropertyTypeDefaultDescription
schemaFieldDefinition[] | JSONSchemaField definitions
initialValuesPartial<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
clearOnHidebooleanfalseReset field value when it becomes hidden
onSubmit(values: T) => void | Promise<void>Called after successful validation
persistfalse | 'session' | 'local'falsePersist values to sessionStorage / localStorage
persistKeystringautoStorage key prefix
debugbooleanfalseLog state changes to console.group

Return value

PropertyTypeDescription
fieldsComputedRef<FieldDefinition[]>Fields after conditions are evaluated
valuesRef<T>Current form values
errorsRef<Record<string, string[]>>Validation errors keyed by field name
touchedRef<Record<string, boolean>>Fields that have been blurred
optionsLoadingRef<Record<string, boolean>>Async options loading state per field
isDirtyComputedRef<boolean>true when values differ from initial state
isValidComputedRef<boolean>true when all visible fields pass validation
isSubmittingRef<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

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 / constselect, 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').