Skip to content

useSchema ​

Takes one FieldDefinition[] and gives back everything derived from it: the form fields, the table columns, the groups and a Zod schema. All reactive.

Open in demo

Import ​

ts
import { useSchema } from '@core/schema/useSchema'

Signature ​

ts
function useSchema(fields: MaybeRefOrGetter<FieldDefinition[]>): {
  fieldList: ComputedRef<FieldDefinition[]>
  zodSchema: ComputedRef<ZodObject>
  formFields: ComputedRef<FieldDefinition[]>
  tableColumns: ComputedRef<FieldDefinition[]>
  fieldGroups: ComputedRef<FieldGroup[]>
}

fields is a MaybeRefOrGetter, so a plain array, a ref or a getter all work. A getter is what makes a schema that changes with the language or a permission stay live.

Returns ​

MemberTypeDescription
fieldListComputedRef<FieldDefinition[]>Everything you passed, unfiltered.
formFieldsComputedRef<FieldDefinition[]>Without hideInForm fields.
tableColumnsComputedRef<FieldDefinition[]>Without hideInTable fields.
zodSchemaComputedRef<ZodObject>Generated from formFields only.
fieldGroupsComputedRef<FieldGroup[]>formFields bucketed by group, in first-seen order.

FieldGroup ​

KeyTypeDescription
namestring | undefinedThe section or tab name. undefined means ungrouped, and renders no heading.
fieldsFieldDefinition[]The fields in that group.

Example ​

vue
<script setup lang="ts">
import type { FieldDefinition } from '@core/schema/types'
import { useSchema } from '@core/schema/useSchema'

const fields: FieldDefinition[] = [
  { key: 'id', type: 'text', label: 'ID', hideInForm: true },
  { key: 'name', type: 'text', label: 'Full name', required: true, group: 'Person' },
  { key: 'email', type: 'email', label: 'Email', required: true, group: 'Person' },
  { key: 'notes', type: 'textarea', label: 'Notes', hideInTable: true, group: 'Extra' },
]

const { formFields, tableColumns, fieldGroups, zodSchema } = useSchema(fields)

const result = zodSchema.value.safeParse({ name: 'Amina Haddad', email: 'amina@example.com' })
</script>

<template>
  <div class="space-y-3">
    <p>{{ formFields.length }} inputs, {{ tableColumns.length }} columns</p>

    <section v-for="group in fieldGroups" :key="group.name ?? 'ungrouped'">
      <h3 v-if="group.name">
        {{ group.name }}
      </h3>
      <ul>
        <li v-for="field in group.fields" :key="field.key">
          {{ field.label }}
        </li>
      </ul>
    </section>

    <p>Valid: {{ result.success }}</p>
  </div>
</template>

Notes ​

  • The policy of validating only what the form renders lives here, not in the generator. generateZodSchema is pure and filters nothing; useSchema hands it formFields. That is why a field hidden from the form can never block a submit.
  • tableColumns keeps fields that are hidden in the form. A column can exist without an input — a created-at stamp is the usual case.
  • Grouping runs over formFields, so a field hidden from the form cannot create an empty section.
  • Groups come out in the order their first field appears, not alphabetically, so the schema's order is the page's order.
  • Everything is a computed. Change the ref or the getter and the form, the table and the validation all follow in the same tick.
  • Schema — every field type and option.
  • Forms — SchemaForm, which is built on this.
  • useCrud — the same fields, used for API validation.
  • DataTable — where tableColumns ends up.