Sin additionalProperties, la validación de esquema no valida nada
En resumen: Una validación exitosa no significa datos limpios. Pydantic ignora los campos desconocidos por defecto, Zod los elimina y Ajv los deja pasar — los parámetros alucinados y las claves mal escritas atraviesan en silencio. Este artículo muestra las tres formas de aceptación silenciosa y da recetas completas: cerrar objetos, controlar la coerción y apretar cada nivel de anidación.

Conclusión: por defecto, los validadores ignoran los campos no declarados en lugar de rechazarlos. Sin additionalProperties: false, una validación exitosa solo prueba que los campos declarados están bien.
El comportamiento más peligroso de un validador no es lanzar un error, sino callarse. Una clave mal escrita o un parámetro inventado por el modelo atraviesa la validación sin ruido y solo explota en la lógica de negocio.
Uno: una validación que "pasa"
{
"type": "object",
"properties": {
"order_id": { "type": "string" },
"amount": { "type": "number" }
},
"required": ["order_id", "amount"]
}
Envíe {"order_id": "A1", "amount": 50, "reasn_code": "damaged"} — la tercera clave es un error tipográfico. Con la configuración por defecto de Ajv, el resultado es un éxito. El campo extra no se rechaza ni se señala.
En la especificación JSON Schema, el valor por defecto de additionalProperties es true: los campos no declarados están permitidos.
Dos: tres sabores del silencio
| Runtime | Comportamiento por defecto | Consecuencia |
|---|---|---|
| Ajv (JS) | deja pasar | datos sucios llegan a la lógica |
Zod z.object |
elimina | el campo desaparece, el objeto se acorta |
| Pydantic v2 | ignora (extra="ignore") |
ídem, sin rastro en los logs |
Eliminar es más furtivo que dejar pasar. Ambas bibliotecas fallan en silencio de la misma manera, por defecto.
Tres: las llamadas de herramientas LLM amplifican el problema
Los argumentos los genera el modelo, que inventa nombres de campos y pasa "3" como número. Las plataformas definen los parámetros en JSON Schema, pero no fuerzan el cierre del esquema por defecto — el modo strict debe activarse explícitamente, y las restricciones gramaticales solo cubren la salida directa del modelo.
{
"order_id": 4411,
"refund_amount_usd": 50,
"reason_code": "damaged",
"urgent": "yes"
}
Con el objeto cerrado, reason_code y urgent se rechazan y el error apunta a /reason_code.
Cuatro: cómo cerrar
Ajv: añada additionalProperties: false en el nodo del 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 o .strict(). Pydantic: ConfigDict(extra="forbid").
Regla común: el cierre es por nivel. Cada objeto anidado necesita su propio additionalProperties: false.
Cinco: política de coerción
El modo lax de Pydantic convierte "5" en 5. Tres políticas: estricta (rechazar tipos incorrectos), controlada (solo conversiones explícitas y acotadas), permisiva (solo datos internos). Para entradas no confiables, elija la primera o la segunda y regístrela en las pruebas.
Seis: lista de verificación de anidación
| Punto | Acción |
|---|---|
| Objeto raíz | additionalProperties: false |
| Cada objeto anidado | ídem |
| Elementos de matriz | cerrar los objetos dentro de items |
| Enums y rangos | añadir enum, minimum/maximum |
| Esquemas generados | completar a mano |
Siete: después del rechazo
allErrors: true reporta todos los campos de una vez; verbose: true añade rutas. Los problemas de sintaxis los atrapa el parser antes de la validación de esquema.
FAQ
¿Demasiado estricto?
Depende del contrato. Para entradas propias, cierre; para datos de terceros extensibles, cierre el núcleo y aísle la zona de extensión.
¿El strip de Zod no es más seguro?
No: protege al downstream a costa del llamador. Los errores deben aparecer pronto y explícitamente.
¿El modo strict de la plataforma sustituye la validación local?
No. El esquema puede seguir abierto y la gramática puede no cubrir los argumentos de las herramientas.
Los datos antiguos fallan en masa tras cerrar?
Es exactamente lo que había que exponer. Inventaríe con allErrors, añada los campos inofensivos, corrija los dañinos.
¿Una solución de una línea?
No, pero el gesto mínimo: verificar que cada type: "object" vaya seguido de additionalProperties: false.