Skip to content

Advanced validation

QForm Builder exposes a serializable validation contract for native FormKit rules, guarded regular expressions, cross-field comparisons, and business rules supplied by the host application.

Architecture principle

The saved schema contains JSON data only:

ts
{
  validation: 'required|multiple_of:5|qform_compare:minimum,gte',
  validationMessages: {
    multiple_of: 'The value must be a multiple of 5.',
  },
}

TypeScript functions are never stored in the schema. They are declared in a runtime registry passed to FormBuilder and FormViewer through validation-rules.

This separation keeps forms safe to save, import, and export without serializing executable code.

Built-in rules

The Advanced validation panel includes:

  • email, url, and number;
  • letters, alphanumeric, lowercase, and uppercase;
  • contains number, lowercase, uppercase, or symbol;
  • accepted, allowed, or forbidden value;
  • starts with and ends with;
  • regular expression;
  • comparison with another field.

The existing required, minimum, maximum, and exact value controls remain available in the same panel.

Guarded regular expressions

The builder stores a regex with the internal qform_regex rule:

ts
validation: 'required|qform_regex:%5E[A-Z]%7B3%7D-%5Cd%7B4%7D%24,i'

The panel automatically encodes the pattern and flags. Before execution, it applies these safeguards:

  • maximum pattern length of 256 characters;
  • flags limited to i, m, s, and u;
  • backreferences and lookbehinds rejected;
  • common catastrophic-backtracking patterns rejected;
  • pattern compiled with RegExp before it is accepted.

These browser-side checks reduce risk, but frontend regex validation never replaces server-side validation.

Cross-field validation

The qform_compare rule compares the current field value with another field in the same form.

Available operators:

OperatorMeaning
eqequal
neqnot equal
gtgreater than
gtegreater than or equal
ltless than
lteless than or equal

Serialized example:

ts
{
  $formkit: 'q-input',
  name: 'confirmation',
  label: 'Confirmation',
  validation: 'required|qform_compare:password,eq',
}

The engine uses the FormKit node graph. When the target field changes, the rule is reactively evaluated again.

Public rule registry

Declare a typed registry in the host application:

ts
import type { QFormBuilderValidationRuleRegistry } from '@vevedh/qform-builder-layer/types'

export const validationRules: QFormBuilderValidationRuleRegistry = [
  {
    key: 'multiple_of',
    label: { fr: 'Multiple de', en: 'Multiple of' },
    description: {
      fr: 'Vérifie qu’une valeur est un multiple du diviseur.',
      en: 'Checks that a value is a multiple of the divisor.',
    },
    category: 'custom',
    arguments: [
      {
        key: 'divisor',
        label: { fr: 'Diviseur', en: 'Divisor' },
        type: 'number',
        required: true,
        defaultValue: '5',
      },
    ],
    handler: (node, divisor = '1') => {
      const value = Number(node.value)
      const normalizedDivisor = Number(divisor)

      return Number.isFinite(value)
        && Number.isFinite(normalizedDivisor)
        && normalizedDivisor !== 0
        && value % normalizedDivisor === 0
    },
    messages: {
      fr: ({ args }) => `La valeur doit être un multiple de ${args[0] || '1'}.`,
      en: ({ args }) => `The value must be a multiple of ${args[0] || '1'}.`,
    },
  },
]

A rule key must match:

text
^[a-z][a-z0-9_-]{0,63}$

A custom rule may be synchronous or return a Promise<boolean>.

Using the registry in builder and viewer

Pass the same registry to both components:

vue
<script setup lang="ts">
import { validationRules } from '~/validation/rules'

const schema = ref([])
const values = ref<Record<string, unknown>>({})
</script>

<template>
  <FormBuilder
    v-model:schema="schema"
    v-model:values="values"
    :validation-rules="validationRules"
  />

  <FormViewer
    v-model="values"
    :form-fields="schema"
    :validation-rules="validationRules"
  />
</template>

Without the runtime registry, a custom rule stored in JSON has no function to execute.

Custom messages

Each field may override a rule’s default message:

ts
{
  validation: 'required|multiple_of:5',
  validationMessages: {
    required: 'This amount is required.',
    multiple_of: 'The amount must increase in steps of 5.',
  },
}

Message priority:

  1. field-level message;
  2. localized public-registry message;
  3. native FormKit message.

Public helpers

The validation subpath exports low-level helpers:

ts
import {
  createQFormBuilderValidationRuleToken,
  parseQFormBuilderValidation,
  removeQFormBuilderValidationRule,
  resolveQFormBuilderValidationRegistry,
  upsertQFormBuilderValidationRule,
  validateQFormBuilderRegex,
} from '@vevedh/qform-builder-layer/validation'

Use these helpers instead of partial string matching. For example, removing min must not accidentally remove an unrelated rule whose key contains the same text.

Security and server-side validation

The registry is trusted code supplied by the host application. Imported JSON cannot inject validationRules functions: the runtime-only property is explicitly rejected by the import sanitizer.

Frontend validation improves UX, but data must always be validated again on the server, ideally through a Zod schema in the relevant Feathers/NFZ service.

QForm Builder — Nuxt 4, Quasar and FormKit layer for dynamic forms.