Validation
SchemaForms has three layers of validation:
- Native rules —
obrigatorio,validacao(minLength, regex, date, etc.) - Custom validators — async functions registered in
validatorMapper, referenced by the schema - Warnings — same as validators, but they don't block submit
Native rules
Configure via FormField.validacao:
interface FieldValidation {
minLength?: number;
maxLength?: number;
min?: number; // numeric minimum
max?: number; // numeric maximum
regex?: string; // regex pattern
regexMessage?: string; // message shown when the regex fails
minDate?: string; // ISO date string
maxDate?: string;
fileTypes?: string[]; // e.g. ['image/png', 'image/jpeg'] — validated before upload
maxFileSize?: number; // bytes
minAge?: number; // minimum age in years (DATE fields)
maxAge?: number;
}
Example in a schema:
{
"nome": "bio",
"tipo": "textarea",
"obrigatorio": true,
"validacao": {
"minLength": 20,
"maxLength": 500
}
}
Custom validators (validate[])
For validation that can't be expressed as a static rule — async checks, cross-field validation, API calls.
Scope:
validate[]works on simple fields, fields insiderepeatablecontainers, and sub-fields offield_arrayitems.
1. Register the functions
Pass a validatorMapper to <FormRenderer>:
import { FormRenderer } from "@schema-forms-data/renderer";
<FormRenderer
schema={schema}
validatorMapper={{
// Async: check email uniqueness
emailUnico: async (value) => {
const exists = await api.checkEmail(String(value));
return exists ? "Email already registered" : undefined;
},
// Cross-field: compare with another field
senhasIguais: (value, allValues, config) => {
const other = allValues[config["field"] as string];
return value === other ? undefined : "Passwords do not match";
},
// With extra schema params
minIdade: (value, _all, config) => {
const age = calculateAge(String(value));
const min = (config["min"] as number) ?? 18;
return age >= min ? undefined : `Minimum age is ${min}`;
},
// With externalData
cpfNoEvento: async (value, _all, _cfg, external) => {
const exists = await api.checkCpf(
String(value),
String(external["evento.id"]),
);
return exists ? "CPF already registered for this event" : undefined;
},
}}
onComplete={handleComplete}
/>;
2. Reference in the schema
Add a validate array to the FormField:
{
"nome": "email",
"tipo": "email",
"obrigatorio": true,
"validate": [
{ "type": "emailUnico", "message": "This email is already in use" }
]
}
{
"nome": "confirmar_senha",
"tipo": "password",
"obrigatorio": true,
"validate": [{ "type": "senhasIguais", "field": "senha" }]
}
{
"nome": "data_nascimento",
"tipo": "date",
"obrigatorio": true,
"validate": [
{
"type": "minIdade",
"min": 16,
"message": "Must be at least 16 years old"
}
]
}
Validator function signature
type FieldValidatorFn = (
value: unknown,
allValues: Record<string, unknown>, // all field values in the current step
config: FieldValidatorConfig, // { type, message, ...extra params }
externalData: Record<string, unknown>,
) => string | undefined | Promise<string | undefined>;
Return undefined for valid, or an error message string.
The message in the schema config (if set) overrides the string returned by the function.
Multiple validators in sequence
Validators run in order — the first one that returns an error message stops the chain:
{
"nome": "cpf",
"tipo": "cpf",
"validate": [
{ "type": "cpfValido" },
{ "type": "cpfUnico", "message": "CPF already registered" }
]
}
Warnings (warn[])
Warnings are messages shown below the field that do not block submit. The API is identical to validate[], but uses the warn key:
{
"nome": "senha",
"tipo": "password",
"warn": [{ "type": "senhaFraca" }]
}
validatorMapper={{
senhaFraca: (value) => {
const s = String(value);
if (s.length < 8) return "Weak password — use at least 8 characters";
if (!/[A-Z]/.test(s)) return "Consider adding an uppercase letter";
return undefined;
},
}}
Warnings are available via:
useFormState().warningsuseField(name).warninguseRendererContext().fieldWarnings
{
"nome": "senha",
"tipo": "password",
"obrigatorio": true,
"warn": [{ "type": "senhaFraca" }]
}
The warning is shown to the user but they can still click Submit.
Server errors (422)
After submit, if the backend returns field errors (e.g. HTTP 422), pass them via fieldErrors:
const [serverErrors, setServerErrors] = useState<Record<string, string>>({});
<FormRenderer
schema={schema}
fieldErrors={serverErrors}
onComplete={async (values) => {
try {
await api.submit(values);
} catch (err) {
if (err.status === 422) {
setServerErrors(err.errors); // { email: 'Invalid email' }
}
}
}}
/>;
Cross-field validation example
validatorMapper={{
dataFimAposInicio: (value, allValues) => {
const inicio = allValues["data_inicio"] as string;
if (!inicio || !value) return undefined;
return new Date(value as string) > new Date(inicio)
? undefined
: "End date must be after the start date";
},
}}
Schema:
{
"nome": "data_fim",
"tipo": "date",
"validate": [{ "type": "dataFimAposInicio" }]
}
Validation execution order
Per step, when advancing or submitting:
obrigatorio(required field with no value)validacaorules (minLength, regex, etc.)- Functions in
validate[](run in parallel) - Functions in
warn[](run in parallel, don't block)
Fields hidden by conditionals are skipped during validation.
Regras nativas
Configure via FormField.validacao:
interface FieldValidation {
minLength?: number;
maxLength?: number;
min?: number; // mínimo numérico
max?: number; // máximo numérico
regex?: string; // padrão regex
regexMessage?: string; // mensagem quando o regex falha
minDate?: string; // ISO date string
maxDate?: string;
fileTypes?: string[]; // ex: ['image/png', 'image/jpeg'] — validado antes do upload
maxFileSize?: number; // bytes
minAge?: number; // idade mínima em anos (campos DATE)
maxAge?: number;
}
Exemplo no schema:
{
"nome": "bio",
"tipo": "textarea",
"obrigatorio": true,
"validacao": {
"minLength": 20,
"maxLength": 500
}
}
Validadores customizados (validate[])
Para validação que não pode ser expressa como regra estática — verificações async, validação cross-field, chamadas de API.
Escopo:
validate[]funciona em campos simples, campos dentro de containersrepeatablee sub-campos de itensfield_array.
1. Registre as funções
Passe um validatorMapper para o <FormRenderer>:
import { FormRenderer } from "@schema-forms-data/renderer";
<FormRenderer
schema={schema}
validatorMapper={{
// Async: verificar unicidade de e-mail
emailUnico: async (value) => {
const exists = await api.checkEmail(String(value));
return exists ? "E-mail já cadastrado" : undefined;
},
// Cross-field: comparar com outro campo
senhasIguais: (value, allValues, config) => {
const outro = allValues[config["field"] as string];
return value === outro ? undefined : "As senhas não conferem";
},
// Com parâmetros extras do schema
minIdade: (value, _all, config) => {
const idade = calcularIdade(String(value));
const min = (config["min"] as number) ?? 18;
return idade >= min ? undefined : `Mínimo ${min} anos`;
},
// Com externalData
cpfNoEvento: async (value, _all, _cfg, external) => {
const exists = await api.checkCpf(
String(value),
String(external["evento.id"]),
);
return exists ? "CPF já inscrito neste evento" : undefined;
},
}}
onComplete={handleComplete}
/>;
2. Referencie no schema
Adicione um array validate ao FormField:
{
"nome": "email",
"tipo": "email",
"obrigatorio": true,
"validate": [
{ "type": "emailUnico", "message": "Este e-mail já está em uso" }
]
}
{
"nome": "confirmar_senha",
"tipo": "password",
"obrigatorio": true,
"validate": [{ "type": "senhasIguais", "field": "senha" }]
}
{
"nome": "data_nascimento",
"tipo": "date",
"obrigatorio": true,
"validate": [{ "type": "minIdade", "min": 16 }]
}
Tipo da função validadora
type FieldValidatorFn = (
value: unknown,
allValues: Record<string, unknown>, // todos os valores do step atual
config: FieldValidatorConfig, // { type, message, ...params }
externalData: Record<string, unknown>,
) => string | undefined | Promise<string | undefined>;
Retorne undefined para "válido" ou uma string de mensagem de erro.
Avisos (warn[])
Avisos são mensagens que aparecem abaixo do campo mas não impedem o submit. A interface e a API são idênticas ao validate[], mas usam a chave warn:
{
"nome": "senha",
"tipo": "password",
"warn": [{ "type": "senhaFraca" }]
}
validatorMapper={{
senhaFraca: (value) => {
const senha = String(value);
return senha.length < 12 ? "Senha curta — considere usar mais de 12 caracteres" : undefined;
},
}}
Acesse os avisos ativos via useFormState():
import { useFormState } from "@schema-forms-data/renderer";
const { warnings } = useFormState();
// warnings = { senha: 'Senha curta — ...' }
Erros do servidor (422)
Após o submit, se o backend retornar erros de campo (ex: HTTP 422), passe-os via fieldErrors:
const [serverErrors, setServerErrors] = useState<Record<string, string>>({});
<FormRenderer
schema={schema}
fieldErrors={serverErrors}
onComplete={async (values) => {
try {
await api.submit(values);
} catch (err) {
if (err.status === 422) {
setServerErrors(err.errors); // { email: 'E-mail inválido' }
}
}
}}
/>;
Ordem de execução da validação
Por step, ao avançar ou submeter:
- Validação de
obrigatorio(campo required sem valor) - Regras de
validacao(minLength, regex, etc.) - Funções em
validate[](executadas em paralelo) - Funções em
warn[](executadas em paralelo, não bloqueiam)
Campos ocultos por condicionais são ignorados na validação.
{
"nome": "confirmacao_senha",
"tipo": "password",
"validate": [{ "type": "senhasIguais", "field": "senha" }]
}
{
"nome": "data_nascimento",
"tipo": "date",
"validate": [
{
"type": "minIdade",
"min": 16,
"message": "Must be at least 16 years old"
}
]
}
Validator function signature
type FieldValidatorFn = (
value: unknown,
allValues: Record<string, unknown>, // all field values in the current step
config: FieldValidatorConfig, // { type, message, ...extra params }
externalData: Record<string, unknown>, // externalData prop
) => string | undefined | Promise<string | undefined>;
Return undefined if valid, or a string error message if invalid.
The message in the schema config (if set) overrides the string returned by the function.
Multiple validators in sequence
Validators run in order — the first one that returns an error message stops the chain:
{
"nome": "cpf",
"tipo": "cpf",
"validate": [
{ "type": "cpfValido" },
{ "type": "cpfUnico", "message": "CPF already registered" }
]
}
Warnings (warn[])
Same as validate[] but the messages don't block submit — they're advisory only.
Warnings are available via:
useFormState().warningsuseField(name).warninguseRendererContext().fieldWarnings
Example
<FormRenderer
schema={schema}
validatorMapper={{
senhaFraca: (value) => {
const s = String(value);
if (s.length < 8) return "Weak password — use at least 8 characters";
if (!/[A-Z]/.test(s)) return "Consider adding an uppercase letter";
return undefined;
},
}}
onComplete={handleComplete}
/>
Schema:
{
"nome": "senha",
"tipo": "password",
"obrigatorio": true,
"warn": [{ "type": "senhaFraca" }]
}
The warning is shown to the user but they can still click Submit.
Cross-field validation example
validatorMapper={{
dataFimAposInicio: (value, allValues) => {
const inicio = allValues["data_inicio"] as string;
if (!inicio || !value) return undefined;
return new Date(value as string) > new Date(inicio)
? undefined
: "End date must be after the start date";
},
}}
Schema:
{
"nome": "data_fim",
"tipo": "date",
"validate": [{ "type": "dataFimAposInicio" }]
}