Sem additionalProperties, a validação de schema não valida nada
Resumo: Validação bem-sucedida não significa dados limpos. O Pydantic ignora campos desconhecidos por padrão, o Zod os remove e o Ajv deixa passar — parâmetros alucinados e chaves com erro de digitação atravessam em silêncio. Este artigo mostra as três formas de aceitação silenciosa e dá receitas completas: fechar objetos, controlar coerção e apertar cada nível de aninhamento.

Conclusão: por padrão, os validadores ignoram campos não declarados em vez de rejeitá-los. Sem additionalProperties: false, uma validação bem-sucedida só prova que os campos declarados estão corretos.
O comportamento mais perigoso de um validador não é levantar erro, é ficar em silêncio. Uma chave com erro de digitação ou um parâmetro inventado pelo modelo atravessa a validação sem barulho e só explode na lógica de negócio.
Um: uma validação que "passa"
{
"type": "object",
"properties": {
"order_id": { "type": "string" },
"amount": { "type": "number" }
},
"required": ["order_id", "amount"]
}
Envie {"order_id": "A1", "amount": 50, "reasn_code": "damaged"} — a terceira chave é um erro de digitação. Com a configuração padrão do Ajv, o resultado é sucesso. O campo extra não é rejeitado nem sinalizado.
Na especificação JSON Schema, o valor padrão de additionalProperties é true: campos não declarados são permitidos.
Dois: três sabores de silêncio
| Runtime | Comportamento padrão | Consequência |
|---|---|---|
| Ajv (JS) | deixa passar | dados sujos chegam à lógica |
Zod z.object |
remove | o campo desaparece, o objeto encolhe |
| Pydantic v2 | ignora (extra="ignore") |
idem, sem rastro nos logs |
Remover é mais furtivo que deixar passar. Ambas as bibliotecas falham em silêncio da mesma forma, por padrão.
Três: chamadas de ferramentas LLM amplificam o problema
Os argumentos são gerados pelo modelo, que inventa nomes de campos e passa "3" como número. As plataformas definem parâmetros em JSON Schema, mas não forçam o fechamento do schema por padrão — o modo strict precisa ser ativado explicitamente, e as restrições gramaticais cobrem apenas a saída direta do modelo.
{
"order_id": 4411,
"refund_amount_usd": 50,
"reason_code": "damaged",
"urgent": "yes"
}
Com o objeto fechado, reason_code e urgent são rejeitados e o erro aponta para /reason_code.
Quatro: como fechar
Ajv: adicione additionalProperties: false no nó do objeto.
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const schema = {
type: "object",
properties: { order_id: { type: "string" }, amount: { type: "number" } },
required: ["order_id", "amount"],
additionalProperties: false,
};
const validate = ajv.compile(schema);
validate({ order_id: "A1", amount: 50, reasn_code: "x" }); // false
Zod: z.strictObject ou .strict(). Pydantic: ConfigDict(extra="forbid").
Regra comum: o fechamento é por nível. Cada objeto aninhado precisa do seu próprio additionalProperties: false.
Cinco: política de coerção
O modo lax do Pydantic converte "5" em 5. Três políticas: estrita (rejeitar tipos errados), controlada (apenas conversões explícitas e limitadas), permissiva (só dados internos). Para entradas não confiáveis, escolha a primeira ou a segunda e registre nos testes.
Seis: checklist de aninhamento
| Ponto | Ação |
|---|---|
| Objeto raiz | additionalProperties: false |
| Cada objeto aninhado | idem |
| Elementos de array | fechar objetos dentro de items |
| Enums e faixas | adicionar enum, minimum/maximum |
| Schemas gerados | completar à mão |
Sete: após a rejeição
allErrors: true reporta todos os campos de uma vez; verbose: true acrescenta caminhos. Problemas de sintaxe devem ser capturados antes, pelo parser.
FAQ
Não é estrito demais?
Depende do contrato. Para entradas próprias, feche; para dados de terceiros com extensões, feche o núcleo e isole a área de extensão.
O strip do Zod não é mais seguro?
Não: protege o downstream às custas do chamador. Erros devem aparecer cedo e explicitamente.
O modo strict da plataforma substitui a validação local?
Não. O schema pode continuar aberto e a gramática pode não cobrir os argumentos das ferramentas.
Dados antigos falham em massa depois de fechar?
É exatamente o que precisava ser exposto. Inventarie com allErrors, adicione campos inofensivos, corrija os nocivos.
Solução de uma linha?
Não, mas o gesto mínimo é: verificar que cada type: "object" vem seguido de additionalProperties: false.