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:
{
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, andnumber;- 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:
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, andu; - backreferences and lookbehinds rejected;
- common catastrophic-backtracking patterns rejected;
- pattern compiled with
RegExpbefore 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:
| Operator | Meaning |
|---|---|
eq | equal |
neq | not equal |
gt | greater than |
gte | greater than or equal |
lt | less than |
lte | less than or equal |
Serialized example:
{
$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:
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:
^[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:
<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:
{
validation: 'required|multiple_of:5',
validationMessages: {
required: 'This amount is required.',
multiple_of: 'The amount must increase in steps of 5.',
},
}Message priority:
- field-level message;
- localized public-registry message;
- native FormKit message.
Public helpers
The validation subpath exports low-level helpers:
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.