Validación de Input: Schemas en 3 Lenguajes
Valida el input de usuarios en el límite de la API con librerías de schemas. Ejemplos con Pydantic, Zod y Jakarta Validation, sanitización y errores estructurados.
Visión General
Todo ataque de inyección, registro corrupto y 500 incomprensible empieza igual: entraron datos al sistema con una forma que nadie verificó. La validación de input rechaza datos malformados, hostiles o absurdos en el límite — antes de que lleguen a la lógica de negocio, a la base de datos o a un log de errores que no dice nada.
La respuesta práctica hoy es la validación schema-first: declaras la forma una vez (“un nombre es un string no vacío de hasta 100 caracteres, una edad es un entero entre 0 y 150”), dejas que una librería haga la verificación y obtienes errores estructurados por campo gratis. Esta receta muestra el mismo schema UserCreate en los tres stacks más usados: Pydantic (Python), Zod (JavaScript/TypeScript) y Jakarta Validation (Java).
Cuándo Usarlo
Usa validación con schemas cuando:
- Aceptas datos de APIs, formularios o uploads de archivos
- Procesas datos externos (JSON, CSV, XML) antes de usarlos
- Aplicas reglas de negocio sobre payloads entrantes (rangos de edad, valores permitidos)
- Conviertes strings no confiables en valores tipados de forma segura
- Devuelves errores por campo que una UI pueda mostrar junto a cada input
Para trabajo de sanitización más profundo — escapar HTML, eliminar caracteres de control, normalizar Unicode — combina la validación con sanitización de input y consulta la Guía de Seguridad para el stack defensivo completo.
Cuándo No Usarlo
- Límites internos entre dos servicios que controla tu equipo, donde el productor ya está tipado y probado
- Prototipos donde una interfaz TypeScript compartida basta — añade validación en servidor antes de que la toquen usuarios reales
- Rutas críticas de latencia que revalidan el mismo payload en cada capa; valida una vez en el borde y pasa objetos tipados hacia dentro
- Cuando una restricción de base de datos es el límite honesto (p. ej. una regla de unicidad — la DB la posee; el schema solo hace los errores más amigables)
Solución
Python (Pydantic v2)
from pydantic import BaseModel, EmailStr, Field, ValidationError, field_validator
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=0, le=150)
bio: str | None = Field(default=None, max_length=500)
@field_validator('name')
@classmethod
def name_must_not_be_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError('el nombre no puede estar vacío')
return v.strip()
# Uso
try:
user = UserCreate(name="Ada Lovelace", email="ada@example.com", age=36)
print(user.model_dump())
except ValidationError as e:
for err in e.errors():
print(f"{'.'.join(map(str, err['loc']))}: {err['msg']}")
Requiere pip install pydantic[email] para EmailStr.
JavaScript (Zod)
const { z } = require('zod');
const UserCreate = z.object({
name: z.string().trim().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
bio: z.string().max(500).optional(),
});
// Lanza excepción con input inválido
try {
const user = UserCreate.parse({
name: 'Ada Lovelace',
email: 'ada@example.com',
age: 36,
});
console.log(user);
} catch (e) {
console.error('Error de validación:', e.issues);
}
// safeParse devuelve un objeto resultado en lugar de lanzar
const result = UserCreate.safeParse({ name: '', email: 'bad' });
if (!result.success) {
for (const issue of result.error.issues) {
console.error(`${issue.path.join('.')}: ${issue.message}`);
}
}
Java (Jakarta Validation)
import jakarta.validation.*;
import jakarta.validation.constraints.*;
import java.util.Set;
public class UserCreate {
@NotBlank
@Size(max = 100)
private String name;
@NotBlank
@Email
private String email;
@Min(0)
@Max(150)
private int age;
@Size(max = 500)
private String bio;
public UserCreate(String name, String email, int age, String bio) {
this.name = name; this.email = email;
this.age = age; this.bio = bio;
}
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
public String getBio() { return bio; }
}
// Uso
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
UserCreate user = new UserCreate("Ada Lovelace", "ada@example.com", 36, null);
Set<ConstraintViolation<UserCreate>> violations = validator.validate(user);
for (ConstraintViolation<UserCreate> v : violations) {
System.out.println(v.getPropertyPath() + ": " + v.getMessage());
}
Cómo Funciona
El flujo tiene dos puertas, y el orden importa. La validación pregunta “¿tiene la forma correcta?” — las formas incorrectas se rechazan con un 400 y errores por campo. La sanitización pregunta “¿está limpio?” — recortar espacios, escapar HTML, normalizar Unicode ocurre sobre datos que ya pasaron la verificación de forma, antes de que el manejador los almacene o renderice.
Tres propiedades hacen que las librerías de schemas valgan la dependencia:
- El schema es el contrato. Las restricciones viven junto a los campos que gobiernan, así que “¿qué acepta este endpoint?” tiene una respuesta en un solo archivo — no en
ifdispersos. - La coerción es explícita. Pydantic y Zod pueden convertir
"36"en36, pero solo donde tú lo permitas. La coerción implícita es como"0"se vuelve falsy y atraviesa unif (age)como fantasma. - Los errores se agregan.
e.errors()/result.error.issues/ConstraintViolations devuelven todas las violaciones, no la primera — el usuario corrige tres campos en un solo intento en lugar de tres.
Validación no es sanitización, y ninguna de las dos es seguridad por sí sola. Un schema que acepta <script> como bio es válido y peligroso. Sanitiza la salida según el contexto (sanitización de input), parametriza las consultas (prevención de SQL injection) y nunca pongas input de usuario en un mensaje de error sin escaparlo.
Variantes
| Lenguaje | Librería | Estilo | Ideal para |
|---|---|---|---|
| Python | Pydantic v2 | Clases de modelo tipadas | FastAPI, pipelines de datos |
| JS/TS | Zod | Constructor de schemas con inferencia TS | Express, tRPC, tipos compartidos cliente/servidor |
| Java | Jakarta Validation | Anotaciones + Validator | Spring/Jakarta EE, modelos estilo bean |
| Cualquiera | JSON Schema | Contrato agnóstico de lenguaje | APIs públicas, codegen — ver validate-json-schema |
Qué Funciona
- Valida en el límite. Los controllers/middleware rechazan input inválido; la capa de servicio recibe objetos tipados, no strings crudos.
- Prefiere allowlists sobre catch-alls.
z.enum(['draft','published'])gana az.string()con un comentario que dice “debería ser uno de…”. - Devuelve errores estructurados.
{"fields": {"email": "Email inválido"}}gana a undetailen prosa — consulta manejo de errores para formatos de respuesta. - Re-valida siempre en servidor. La validación en cliente es UX, no seguridad.
- Mantén los schemas en módulos compartidos. Si la misma forma se valida en dos endpoints, vive en un solo archivo — dos copias divergen.
Código complementario: El repo companion de input-validation tiene ejemplos ejecutables en Pydantic v2 y Zod, más tests que cubren los caminos de error.
Errores Comunes
- Confiar en la validación del cliente. Mejora la UX; no detiene nada — cualquiera con
curlla salta. - Regex donde corresponde un schema. Regex sirve para formatos dentro de un campo (expresiones regulares); úsalo para patrones, no para parsear payloads estructurados.
- Sorpresas de coerción. Que
age: "abc"falle es esperado; queage: "36"se convierta silenciosamente en36— o siga siendo string — depende del modo estricto de la librería. Conoce el tuyo. - Filtrar internals en mensajes de error. “Column
users.emailviolates unique constraint” le mapea tu esquema a un atacante; devuelve mensajes por campo y registra el detalle. - Validar demasiado tarde. Verificar después de
db.save()deja estado parcial; valida antes de cualquier mutación.
Lectura Adicional
- Pydantic — Validators —
field_validator, validadores de modelo, modo estricto. - Zod — Basic usage — composición de schemas,
safeParse, formato de errores. - Jakarta Validation 3.0 specification — las anotaciones de restricción usadas arriba.
- OWASP Input Validation Cheat Sheet — el checklist del lado de seguridad.
Preguntas frecuentes
¿Debería validar en el controller o en la capa de servicio?
En el límite — controller o middleware. La capa de servicio debería recibir objetos en los que puede confiar; si tu servicio revalida todo, tu validación de borde no está haciendo su trabajo.
¿Cómo valido objetos anidados y arrays?
Las tres librerías componen: Pydantic anida BaseModels, Zod anida z.object()s y z.array()s, Jakarta propaga con @Valid en campos anidados. Define el schema hijo una vez y referéncialo.
¿Cuál es la diferencia entre validación y sanitización?
La validación verifica si el input cumple restricciones y lo rechaza si no. La sanitización transforma el input — escapar HTML, recortar espacios, normalizar Unicode — para que sea seguro almacenarlo o renderizarlo. Valida primero, sanitiza después, y no confundas "rechazado" con "limpiado".
¿Qué librería debería elegir según mi stack?
Pydantic para Python (y viene gratis con FastAPI), Zod para proyectos JS TypeScript-first, Jakarta Validation para Java/Spring. Para un contrato de API pública agnóstico de lenguaje, JSON Schema.
¿Qué código de estado deberían devolver los errores de validación?
400 Bad Request es el estándar; 422 Unprocessable Content cuando la sintaxis es JSON válido pero la semántica falla — común con validación de schemas. Elige uno y mantenlo consistente entre endpoints.
Recursos Relacionados
Manejar Errores en APIs con RFC 7807
Patrones para un manejo de errores de API consistente y predecible en varios lenguajes y frameworks.
RecipeExpresiones Regulares
Cómo usar expresiones regulares para matching de patrones, validación y extracción de texto en Python, JavaScript y Java.
RecipeSanitizar Input de Usuario
Cómo sanitizar y validar input de usuario en Python, Java y JavaScript para prevenir ataques de inyección.
RecipePrevenir Ataques de Inyección SQL
Cómo escribir queries parametrizadas y usar ORMs para eliminar vulnerabilidades de inyección SQL en Python, JavaScript y Java.
RecipeValidacion de Datos Basada en Schemas con Zod en TypeScript
Valida y sanitiza datos entrantes usando schemas Zod con inferencia de TypeScript, refinements custom y formateo de errores para validacion robusta de APIs y formularios
RecipeVersionado de APIs: URL, Header y Media Type
Cómo versionar APIs REST con estrategias de ruta URL, header y media-type manteniendo compatibilidad hacia atrás para los clientes existentes.