Cómo leer errores de validación
Un parseo fallido te devuelve un error. Si lo muestras directo en la consola, parece un muro de texto sin salida.
Pero no lo es. Todo lo que necesitas para mostrar un mensaje junto al campo correcto está ahí, en un formato pensado para que el código lo pueda leer.
Empecemos con un schema que dos valores pueden romper:
import * as z from 'zod'
const teacherSchema = z.object({
name: z.string(),
age: z.number().min(18),
})
const result = teacherSchema.safeParse({ name: 12345, age: 13 })El arreglo de issues
El contenido útil del error vive en .issues, y es un arreglo porque un solo parseo puede encontrar varios problemas a la vez:
console.log(result.error.issues.length)
// 2Dos valores estaban mal, así que hay dos issues. Cada uno es un objeto que describe un solo problema:
console.log(result.error.issues[0])
// {
// expected: 'string',
// code: 'invalid_type',
// path: [ 'name' ],
// message: 'Invalid input: expected string, received number'
// }Cuatro campos que vale la pena conocer:
| Campo | Qué te dice |
|---|---|
code | El tipo de falla, como invalid_type o too_small |
path | Qué clave falló, como un arreglo |
message | Una oración que describe el problema |
expected | Qué esperaba el schema, en fallas de tipo |
La comparación con el escáner sigue siendo válida acá. El mensaje de la consola es el resumen que la máquina imprime en su pantalla. issues es el informe detallado que hay debajo, y ese es con el que en realidad trabajas.
El arreglo está ahí todo el tiempo. Si usas .issues obtienes la versión estructurada, con una entrada por cada cosa que salió mal.
Qué campo falló
path es un arreglo en vez de un string, porque una clave puede estar anidada:
const orderSchema = z.object({
user: z.object({
profile: z.object({
email: z.email(),
}),
}),
})
const bad = orderSchema.safeParse({ user: { profile: { email: 'nope' } } })
console.log(bad.error.issues[0].path)
// [ 'user', 'profile', 'email' ]En un formulario plano el primer elemento es el nombre del campo, y eso alcanza para construir lo que un formulario necesita:
const errors = result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
}))
console.log(errors)
// [
// { field: 'name', message: 'Invalid input: expected string, received number' },
// { field: 'age', message: 'Too small: expected number to be >=18' }
// ]Un nombre de campo y una oración, uno por problema. Ese es el formato que necesita un formulario para colocar cada mensaje junto al input al que corresponde.
['user', 'profile', 'email'] es una serie de indicaciones: entra a user, luego a profile, luego a email. Un simple string no podría decir eso sin que tú tuvieras que volver a descomponerlo.
Escribe tus propios mensajes
Los mensajes por defecto describen el sistema de tipos, no tu formulario. "Invalid input: expected string, received number" es preciso, pero no le sirve a ningún usuario real.
Casi todos los métodos de Zod aceptan un mensaje como argumento:
const teacherSchema = z.object({
name: z.string('Por favor ingresa tu nombre'),
age: z.number().min(18, 'Los profesores deben tener al menos dieciocho años'),
})
const result = teacherSchema.safeParse({ name: 12345, age: 13 })
console.log(result.error.issues.map((issue) => issue.message))
// [ 'Por favor ingresa tu nombre', 'Los profesores deben tener al menos dieciocho años' ]El mensaje reemplaza el valor por defecto solo para esa comprobación. Cada restricción tiene el suyo propio, así que un campo puede decir una cosa cuando falta y otra cuando es demasiado corto.
Quien lee esto no está depurando tu schema. Está tratando de registrarse.
Ponlo en práctica
Dado este schema y un personaje que rompe sus dos reglas:
const characterSchema = z.object({
name: z.string('Todo personaje necesita un nombre'),
episode: z.number().min(1, 'Los episodios empiezan en 1'),
})
const character = { name: 42, episode: 0 }Seis espacios en blanco, marcados con ___. Cada uno de success, result, error, issues, path y message encaja exactamente en uno de ellos:
const ___ = characterSchema.safeParse(character)
if (result.___) {
console.log('All good')
} else {
console.log(
result.___.___.map((issue) => ({
field: issue.___[0],
message: issue.___,
})),
)
}Compara tus respuestas
const result = characterSchema.safeParse(character)
if (result.success) {
console.log('All good')
} else {
console.log(
result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
})),
)
}Resolviéndolo en orden:
- El primer espacio nombra lo que devuelve
safeParse, y todo lo que sigue usaresult, así que tiene que serresult. result.successes el booleano que decide qué rama se ejecuta.result.errorsolo existe en la rama fallida, por eso va dentro delelse..issueses el arreglo, así que.maplo necesita.issue.path[0]es el nombre del campo, con índice porquepathes un arreglo.issue.messagees la oración.
La cadena se lee como una sola oración cuando encaja: los issues del error del resultado, cada uno con un path y un message.
Las dos reglas fallan, así que se registran dos entradas:
// [
// { field: 'name', message: 'Todo personaje necesita un nombre' },
// { field: 'episode', message: 'Los episodios empiezan en 1' }
// ]Hacia dónde va esto ahora
Tres capítulos de Zod, todos ejecutados a mano sobre objetos inventados. El schema está haciendo trabajo real, pero todavía no está conectado a la aplicación.
Dónde encaja la validación la conecta con la aplicación, ubicando la comprobación entre una solicitud entrante y el código que actúa sobre ella.

