Publicado em

- 7 minutos de leitura

validateStandardSchema com Signal Forms

img of validateStandardSchema com Signal Forms

O Angular 22 trouxe os Signal Forms para um ponto mais maduro da API de formulários.

Dentro desse pacote, a função validateStandardSchema resolve um problema comum: reutilizar schemas de validação já existentes, como schemas do Zod ou do Valibot, diretamente dentro de um formulário baseado em Signals.

Em vez de reescrever as mesmas regras usando apenas validadores nativos do Angular, você pode declarar um schema externo e deixar o Signal Forms consumir esse contrato.

Isso reduz duplicação, melhora consistência e aproxima a validação do frontend de validações que já podem existir em outras camadas da aplicação.

Se preferir, assista o vídeo diretamente no YouTube:

O que é a função validateStandardSchema

validateStandardSchema é uma função do pacote @angular/forms/signals.

Ela valida um campo, ou uma árvore de campos, usando um validador compatível com StandardSchemaV1.

Na prática, isso significa que o Angular não precisa conhecer a API interna do Zod, do Valibot ou de outra biblioteca.

Ele só precisa receber um objeto que siga o contrato da Standard Schema.

O uso básico segue este formato:

   import { form, validateStandardSchema } from '@angular/forms/signals'
import { signal } from '@angular/core'
import * as z from 'zod'

const userSchema = z.object({
	email: z.email('Digite um e-mail válido.'),
	password: z.string().min(8, 'A senha precisa ter pelo menos 8 caracteres.')
})

const model = signal({ email: '', password: '' })

const userForm = form(model, (path) => {
	validateStandardSchema(path, userSchema)
})

O primeiro argumento é o caminho do formulário que será validado.

O segundo argumento é o schema compatível com Standard Schema.

Quando o valor muda, o Signal Forms executa a validação e expõe os erros pelos estados reativos do formulário, como errors(), invalid() e valid().

O que é Standard Schema

Standard Schema é uma especificação para bibliotecas de validação em TypeScript.

A ideia é definir uma interface comum para que ferramentas consigam consumir schemas sem depender diretamente de uma biblioteca específica.

Um schema compatível expõe uma propriedade ~standard.

Essa propriedade informa a versão da spec, o fornecedor da biblioteca, os tipos inferidos e uma função validate.

De forma simplificada, o contrato gira em torno desta ideia:

   interface StandardSchemaV1<Input = unknown, Output = Input> {
	readonly '~standard': {
		readonly version: 1
		readonly vendor: string
		readonly validate: (
			value: unknown
		) =>
			| { value: Output; issues?: undefined }
			| { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<PropertyKey> }> }
			| Promise<
					| { value: Output; issues?: undefined }
					| { issues: ReadonlyArray<{ message: string; path?: ReadonlyArray<PropertyKey> }> }
			  >
	}
}

O ponto importante é que a validação recebe unknown.

Isso faz sentido porque um formulário, uma API ou um arquivo de configuração sempre podem entregar dados inválidos em runtime.

Se a validação passar, o schema retorna um valor tipado.

Se falhar, retorna uma lista de issues com mensagens e, quando possível, o caminho do campo com erro.

É exatamente esse formato que permite ao Angular transformar erros emitidos pelo schema em erros do Signal Forms.

Como Zod e Valibot usam Standard Schema

Bibliotecas como Zod e Valibot criam schemas executáveis em runtime.

Esses schemas validam dados reais, não apenas tipos de TypeScript.

Com a Standard Schema, essas bibliotecas também podem expor uma interface neutra.

Isso permite que ferramentas externas tratem o schema como uma caixa-preta.

A ferramenta não precisa chamar schema.parse(), safeParse() ou v.parse().

Ela chama o método padronizado em schema['~standard'].validate(value) e recebe um resultado também padronizado.

No caso do Angular, essa escolha evita adapters específicos como validateZodSchema ou validateValibotSchema.

O mesmo validateStandardSchema consegue trabalhar com qualquer biblioteca que implemente a spec.

   async function validateWithAnySchema(schema: StandardSchemaV1, value: unknown) {
	const result = await schema['~standard'].validate(value)

	if (result.issues) {
		return result.issues
	}

	return result.value
}

Essa é a principal vantagem da Standard Schema.

Ela cria interoperabilidade.

O Angular consome o contrato.

Zod, Valibot e outras bibliotecas emitem validações nesse contrato.

O desenvolvedor escolhe a biblioteca de schema sem acoplar o formulário a uma integração proprietária.

Imagem com o texto: Curso Formulários com Angular e com a logo do Angular e um formulário. Logo abaixo existe um botão com o texto "Eu quero!"

Como criar um schema com Zod

Vamos criar um schema para um cadastro simples.

O formulário terá nome, e-mail, senha e aceite dos termos.

   import * as z from 'zod'

export const cadastroSchema = z.object({
	nome: z.string().trim().min(3, 'Informe pelo menos 3 caracteres.'),
	email: z.email('Digite um e-mail válido.'),
	senha: z.string().min(8, 'A senha precisa ter pelo menos 8 caracteres.'),
	aceitouTermos: z.boolean().refine((value) => value, 'Você precisa aceitar os termos.')
})

export type Cadastro = z.infer<typeof cadastroSchema>

Esse schema faz duas coisas.

Primeiro, valida os dados em runtime.

Segundo, gera um tipo TypeScript com z.infer.

Esse tipo pode ser usado como modelo do Signal Form.

Isso mantém a estrutura do formulário alinhada com a validação.

Como vincular Zod com Signal Forms

Agora podemos usar esse schema dentro de um componente Angular.

O modelo fica em um signal.

O formulário é criado com form.

A validação externa entra no callback de schema usando validateStandardSchema.

   import { ChangeDetectionStrategy, Component, signal } from '@angular/core'
import { FormField, FormRoot, form, validateStandardSchema } from '@angular/forms/signals'
import { cadastroSchema, type Cadastro } from './cadastro.schema'

@Component({
	selector: 'app-cadastro',
	imports: [FormField, FormRoot],
	template: `
		<form [formRoot]="cadastroForm">
			<label>
				Nome
				<input [formField]="cadastroForm.nome" />
			</label>

			@if (cadastroForm.nome().touched()) {
				@for (error of cadastroForm.nome().errors(); track error) {
					<p>{{ error.message }}</p>
				}
			}

			<label>
				E-mail
				<input type="email" [formField]="cadastroForm.email" />
			</label>

			@if (cadastroForm.email().touched()) {
				@for (error of cadastroForm.email().errors(); track error) {
					<p>{{ error.message }}</p>
				}
			}

			<label>
				Senha
				<input type="password" [formField]="cadastroForm.senha" />
			</label>

			@if (cadastroForm.senha().touched()) {
				@for (error of cadastroForm.senha().errors(); track error) {
					<p>{{ error.message }}</p>
				}
			}

			<label>
				<input type="checkbox" [formField]="cadastroForm.aceitouTermos" />
				Aceito os termos
			</label>

			@if (cadastroForm.aceitouTermos().touched()) {
				@for (error of cadastroForm.aceitouTermos().errors(); track error) {
					<p>{{ error.message }}</p>
				}
			}

			<button type="submit" [disabled]="cadastroForm().invalid()">Criar conta</button>
		</form>
	`
})
export class CadastroComponent {
	protected model = signal<Cadastro>({
		nome: '',
		email: '',
		senha: '',
		aceitouTermos: false
	})

	protected cadastroForm = form(
		this.model,
		(path) => {
			validateStandardSchema(path, cadastroSchema)
		},
		{
			submission: {
				action: async () => {
					const value = this.model()

					console.log('Cadastro válido:', value)

					return null
				}
			}
		}
	)
}

O detalhe principal está aqui:

   cadastroForm = form(
	this.model,
	(path) => {
		validateStandardSchema(path, cadastroSchema)
	},
	{
		submission: {
			action: async () => {
				return null
			}
		}
	}
)

O path representa a árvore do formulário.

Como o schema do Zod também representa a mesma árvore de dados, o Angular consegue associar os erros aos campos corretos.

Se email falhar, o erro aparece em cadastroForm.email().errors().

Se senha falhar, o erro aparece em cadastroForm.senha().errors().

Se uma regra de objeto falhar, o erro pode aparecer no caminho correspondente informado pela biblioteca.

Quando usar validateStandardSchema

Use validateStandardSchema quando o schema já é uma fonte importante de regra de negócio.

Isso acontece bastante quando o mesmo contrato é usado no frontend, no backend, em testes, em mocks ou em validações de payload.

Também faz sentido quando o time já usa Zod ou Valibot para modelar dados externos.

Nesse cenário, reescrever as mesmas regras com validadores específicos do Angular aumenta o risco de divergência.

Por outro lado, validadores nativos do Signal Forms continuam úteis para regras simples, locais e muito acopladas à interação do formulário.

Por exemplo: required, email, minLength, validate, validateAsync e validateHttp.

Você não precisa transformar todo formulário em um schema externo.

O ganho aparece quando a validação precisa ser compartilhada, testada separadamente ou mantida como contrato de domínio.

Schemas dinâmicos com validateStandardSchema

validateStandardSchema também aceita uma função que retorna o schema.

Isso permite criar schemas dinâmicos baseados em Signals.

   import { computed, signal } from '@angular/core'
import { form, validateStandardSchema } from '@angular/forms/signals'
import * as z from 'zod'

const model = signal({
	tipoDocumento: 'cpf' as 'cpf' | 'passaporte',
	documento: ''
})

const documentoSchema = computed(() =>
	z.object({
		tipoDocumento: z.enum(['cpf', 'passaporte']),
		documento:
			model().tipoDocumento === 'cpf'
				? z.string().length(11, 'CPF precisa ter 11 dígitos.')
				: z.string().min(6, 'Passaporte precisa ter pelo menos 6 caracteres.')
	})
)

const documentoForm = form(model, (path) => {
	validateStandardSchema(path, () => documentoSchema())
})

Nesse caso, o schema acompanha o estado atual do formulário.

Quando tipoDocumento muda, a regra aplicada em documento também muda.

Isso mantém a validação declarativa sem espalhar if manual por vários validadores.

Cuidados ao usar schemas externos em formulários

O primeiro cuidado é manter o modelo do formulário compatível com o schema.

Se o schema espera uma estrutura diferente, os erros podem aparecer no lugar errado ou cair no nível raiz do formulário.

O segundo cuidado é lembrar que algumas bibliotecas também transformam dados.

No exemplo com Zod, trim() remove espaços do nome no resultado validado.

Isso não significa necessariamente que o valor bruto digitado no campo será reformatado automaticamente na interface.

Validação e normalização visual são responsabilidades diferentes.

O terceiro cuidado é não esconder regras de experiência do usuário dentro de um schema genérico demais.

Mensagens, estados pendentes e regras dependentes de interação ainda podem ficar mais claras quando escritas diretamente com os validadores do Signal Forms.

Conclusão

validateStandardSchema conecta Signal Forms com o ecossistema moderno de validação em TypeScript.

Com ela, o Angular 22 consegue consumir schemas compatíveis com Standard Schema sem depender diretamente de Zod, Valibot ou qualquer biblioteca específica.

Na prática, isso permite criar formulários mais consistentes, com menos duplicação e com validações reaproveitáveis.

Quando o schema já representa um contrato importante da aplicação, usar validateStandardSchema é uma forma simples de trazer esse contrato para dentro dos Signal Forms.

Referências

Entre na nossa comunidade!

Receba novos posts, novidades do ecossistema Angular e muito mais.

Sobre o autor

Author's photo
Henrique Custódia Arquiteto Frontend, entusiasta Angular, cat lover, criador de conteúdo e fundador da Code Dimension!