Skip to content

Advanced Schema

Schema composition

ts
import { mergeSchemas, omitFields, pickFields, extendField } from '@macrulez/vue-form-schema'

const base = [
  { type: 'text' as const, name: 'firstName' },
  { type: 'text' as const, name: 'lastName' },
  { type: 'email' as const, name: 'email' },
]

// Combine — later schemas win on name collision
const extended = mergeSchemas(base, [{ type: 'text' as const, name: 'phone' }])

// Remove fields
const noEmail = omitFields(base, ['email'])

// Keep only specific fields
const nameOnly = pickFields(base, ['firstName', 'lastName'])

// Non-mutating patch
const required = extendField(base, 'email', { required: true, label: 'Email address' })

Discriminated schemas

A common pattern: the entire set of fields changes based on one "discriminator" field's value — payment method, address type, document type. Wiring visible by hand on every field is verbose and easy to get wrong. discriminatedFields(discriminatorName, variants) builds that visible wiring for you:

ts
import { discriminatedFields } from '@macrulez/vue-form-schema'
import { useForm } from '@macrulez/vue-form-schema'

const schema = [
  {
    type: 'radio' as const,
    name: 'paymentMethod',
    label: 'Payment method',
    options: [
      { label: 'Card', value: 'card' },
      { label: 'PayPal', value: 'paypal' },
    ],
  },
  ...discriminatedFields('paymentMethod', {
    card: [
      { type: 'text' as const, name: 'cardNumber', label: 'Card number', required: true },
      { type: 'text' as const, name: 'cvc', label: 'CVC', required: true },
    ],
    paypal: [
      { type: 'email' as const, name: 'paypalEmail', label: 'PayPal email', required: true },
    ],
  }),
]

const { fields } = useForm({ schema, clearOnHide: true })

discriminatedFields doesn't create the discriminator field itself — define that separately (typically select/radio) and spread the helper's result alongside it. Each returned field's visible is set to "the discriminator's value matches this variant", combined via AND with the field's own visible if it already had one. Pair it with clearOnHide: true on useForm so switching variants resets the now-hidden variant's values.

Native mapping from Zod / Valibot: parseZod accepts a root z.discriminatedUnion(key, [...]) schema, and parseValibot accepts a root v.variant(key, [...]) schema — both convert straight into a discriminator select plus discriminatedFields-wired variant fields:

ts
import { z } from 'zod'
import { parseZod } from '@macrulez/vue-form-schema/zod'

const schema = z.discriminatedUnion('paymentMethod', [
  z.object({ paymentMethod: z.literal('card'), cardNumber: z.string(), cvc: z.string() }),
  z.object({ paymentMethod: z.literal('paypal'), paypalEmail: z.string().email() }),
])

const fields = parseZod(schema)

This only applies when the discriminated union/variant is the root schema passed to parseZod/parseValibot — a discriminated union nested as a property inside a larger z.object({...}) is not expanded automatically; use discriminatedFields directly for that case.

TypeScript inference

From a Zod / Yup / Valibot schema (automatic)

parseZod / parseYup / parseValibot return a FieldDefinition[] that also carries the source schema's inferred value type. useForm({ schema: fields }) picks it up automatically — no explicit useForm<Values>(...) needed:

ts
import { z } from 'zod'
import { parseZod } from '@macrulez/vue-form-schema/zod'
import { useForm } from '@macrulez/vue-form-schema'

const schema = z.object({ username: z.string(), age: z.number() })
const fields = parseZod(schema)

const { values } = useForm({
  schema: fields,
  onSubmit: (data) => {
    data.username // string ✓ — inferred from `schema`, not Record<string, unknown>
  },
})

This works the same way for parseYup (via Yup's InferType) and parseValibot (via Valibot's InferOutput). Passing an explicit useForm<Values>({ schema: fields }) still works and overrides the inferred type if you need to.

From a hand-written FieldDefinition[]

InferValues<T> maps a readonly FieldDefinition[] literal to a typed values object — use this when the schema isn't coming from Zod/Yup/Valibot.

ts
import { defineSchema } from '@macrulez/vue-form-schema'
import type { InferValues } from '@macrulez/vue-form-schema'

const schema = defineSchema([
  { type: 'text' as const, name: 'username' as const },
  { type: 'number' as const, name: 'age' as const },
  { type: 'checkbox' as const, name: 'agreed' as const },
] as const)

type Values = InferValues<typeof schema>
// { username: string; age: number; agreed: boolean }

const { values } = useForm<Values>({ schema })
// values.value.username is string ✓

Type mapping: checkboxboolean, numbernumber, arrayunknown[], everything else → string.