Core Docs
Frontend

Validação de Formulários (Zod)

Padrões usados nos schemas Zod + React Hook Form em todo o app.

Cada feature com formulário tem um schemas/*.schema.ts com um schema Zod, plugado no react-hook-form via zodResolver. Alguns padrões se repetem entre features — vale conhecer antes de criar um schema novo.

Campo obrigatório condicional (.refine())

Quando um campo só é obrigatório dependendo de outro, usa .refine() no schema inteiro, apontando o erro pro campo certo com path:

// candidate.schema.ts
.refine(
  (data) => {
    if (data.hasChildren && (data.childrenCount == null || data.childrenCount < 1)) return false;
    return true;
  },
  { message: 'Informe a quantidade de filhos', path: ['childrenCount'] },
)

Validação cruzada de múltiplos campos (.superRefine())

Quando é preciso adicionar mais de um erro na mesma validação (ou a lógica é mais complexa que um .refine() simples), usa .superRefine() com context.addIssue():

// client.schema.ts — CPF tem que ter 11 dígitos, CNPJ 14, dependendo do personType
const validateDocument = (data, ctx) => {
  const digits = data.cnpjCpf.replace(/\D/g, '');
  if (data.personType === CLIENT_PERSON_TYPE.PJ && digits.length !== 14) {
    ctx.addIssue({ code: 'custom', message: 'CNPJ deve ter 14 dígitos', path: ['cnpjCpf'] });
  }
  if (data.personType === CLIENT_PERSON_TYPE.PF && digits.length !== 11) {
    ctx.addIssue({ code: 'custom', message: 'CPF deve ter 11 dígitos', path: ['cnpjCpf'] });
  }
};

Reestruturar o payload antes de enviar (.transform())

O formulário de vaga (vacancy.schema.ts) é o exemplo mais elaborado: campos como coveragePeriod, registration e completeName aparecem soltos no formulário (mais simples pro usuário preencher), mas o backend espera eles agrupados dentro de details só quando o motive exigir (ver Recrutamento no backend). O .superRefine() valida quais campos são obrigatórios pra cada motive, e o .transform() logo depois reagrupa o payload final:

export const vacancySchema = baseVacancySchema
  .superRefine((vacancy, context) => {
    if ([ABSENCE, BREAK].includes(vacancy.motive) && !vacancy.coveragePeriod) {
      context.addIssue({ code: 'custom', message: 'Preencha o prazo da cobertura', path: ['coveragePeriod'] });
    }
    // ...SUBSTITUTION exige registration + completeName
  })
  .transform((data) => {
    const { coveragePeriod, registration, completeName, ...rest } = data;
    // só inclui `details` se o motive exigir; senão retorna `rest` sem esses campos soltos
    return requiresDetails ? { ...rest, details: { coveragePeriod, ... } } : rest;
  });

export type CreateVacancyFormData = z.input<typeof vacancySchema>;   // tipo ANTES do transform (o que o form usa)
export type SubmitVacancyPayload = z.output<typeof vacancySchema>;    // tipo DEPOIS do transform (o que a API recebe)

z.input<> vs z.output<> é o detalhe importante aqui: como o schema transforma o shape dos dados, o tipo usado no useForm<> (input) é diferente do tipo enviado pro backend (output).

Schema de criação vs. edição

Formulários de edição costumam reaproveitar o schema base com .partial() (tudo opcional) e depois .required({...}) só nos campos que continuam obrigatórios mesmo editando:

// vacancy.schema.ts
export const updateVacancySchema = baseVacancySchema.partial().required({
  motive: true,
  clientId: true,
  state: true,
  city: true,
  // ...
});

Upload de arquivo (imagem de roteiro da vaga)

Não é validado por Zod, mas segue o mesmo padrão de mutation dos formulários — vale registrar aqui por ser o único upload de arquivo do app. useUploadRoadmapImage (features/vacancy/hooks/useUploadImage.tsx) monta um FormData e faz PATCH multipart:

const formData = new FormData();
formData.append('roadmapImage', file);
await api.patch(`/vacancy/${vacancyId}/roadmap-image`, formData, {
  headers: { 'Content-Type': 'multipart/form-data' },
});

No onSuccess, invalida só o vacancyQueryKeys.detail(vacancyId) — não a lista inteira, já que só aquele registro mudou.

On this page