Zod: validación, parsing y modelos para datos de API en runtime
Conocí Zod cuando me sumé a un proyecto en Mapplics que ya estaba avanzado. El equipo lo utilizaba para validar datos en runtime. A medida que conocí la estructura del sistema, empecé a entender cómo Zod ayudaba a tratar datos provenientes de distintas APIs, especialmente cuando podrían existir inconsistencias o estructuras incompletas. La validación de TypeScript es útil durante el desarrollo, pero no cubre esos casos en los que los datos reales llegan con variaciones o errores.
Contar con una capa que valida los datos antes de que Angular los procese ayuda a evitar varios problemas que aparecen más adelante en el flujo de la aplicación.
La validación runtime no reemplaza a TypeScript: lo complementa y cubre los casos en los que los datos dejan de responder al contrato esperado.
Por qué usar validación en runtime en una aplicación Angular grande
En aplicaciones grandes, las fuentes de datos se multiplican. Más endpoints, más servicios externos, más integraciones. Esto aumenta las posibilidades de recibir datos inconsistentes. Cuando la API cambia, cuando una propiedad llega vacía o cuando un tipo no coincide, el sistema empieza a fallar de formas difíciles de rastrear.
Usar Zod permite controlar ese flujo desde el primer momento, antes de que la información ingrese a modelos, servicios o storage.
Problemas comunes que Zod ayuda a evitar
1. Inconsistencias en los datos
Son errores que no aparecen de inmediato. Quizás un campo viene nulo, o un string representa lo que debería ser un número. Angular sigue ejecutando, pero en algún componente aparece un error inesperado.
2. Corrupción de storage
Si los datos se guardan sin validación, es posible que terminen almacenados en un formato incorrecto. Más adelante, cuando la app intente leerlos, el resultado es impredecible.
3. Crashes sin información útil
Un acceso a una propiedad inexistente puede romper la aplicación sin ofrecer mensajes claros sobre la causa.
4. Dificultad para hacer debugging
Rastrear manualmente la fuente de un dato defectuoso puede llevar tiempo y no siempre es evidente dónde se generó el problema.
Aprender lo básico de Zod ayuda a abordar varios de estos problemas sin añadir una complejidad significativa y manteniendo un flujo de trabajo cercano al que se usa habitualmente en Angular.
Una forma práctica de abordar estos problemas es definir los datos mediante schemas claros y consistentes, que actúan como la estructura base de todo el flujo de información dentro de la aplicación.
Schemas como base del sistema
Los schemas definen la estructura que la aplicación espera recibir al trabajar con datos. Cuando un servicio recibe información de una API, el schema valida que cumpla con la estructura esperada antes de que esa información llegue a los modelos o componentes. Esto centraliza la validación y evita dispersar verificaciones en distintas partes del código.
Un patrón útil es crear entidades genéricas que luego pueda ser extendidas por otras entidades.
Entidad genérica (schema base)
import { z } from 'zod';
/**
* Schema para validar la estructura básica de una entidad genérica
*/
export const EntidadGenericaSimpleSchema = z.object({
Id: z.number({ required_error: 'Falta el ID', invalid_type_error: 'El ID debe ser un número' }).int(),
Descripcion: z.string({ required_error: 'Falta la descripción', invalid_type_error: 'Debe ser un texto' })
});
/**
* Schema para validar una entidad genérica con código
*/
export const EntidadGenericaConCodigoSchema = EntidadGenericaSimpleSchema.extend({
Codigo: z.string({ required_error: 'Falta el Codigo', invalid_type_error: 'Debe ser un texto' })
});
/**
* Schema para validar una entidad genérica con tag
*/
export type TEntidadGenericaSimpleJson = z.infer<typeof EntidadGenericaSimpleSchema>;
export type TEntidadGenericaConCodigoJson = z.infer<typeof EntidadGenericaConCodigoSchema>;
export type TEntidadGenericaConTagJson = z.infer<typeof EntidadGenericaConTagSchema>; Extender schemas de esta forma evita repetir lógica y garantiza coherencia en todas las entidades básicas del proyecto.
Validación aplicada a un caso real: Actividades
Las actividades en el proyecto combinan campos genéricos y campos propios. Validarlas ayuda a controlar variaciones en las respuestas de la API.
Schema de Actividades
import { z } from 'zod';
import { ZodHelper } from 'src/app/core/helper/zod-helper/zod.helper';
import { EntidadGenericaConCodigoSchema } from '../entidad-generica/entidad-generica.schema';
export const ActividadesResponseSchema = EntidadGenericaConCodigoSchema.extend({
Tipo: z.string({
required_error: 'Falta el tipo de actividad',
invalid_type_error: 'El tipo de actividad debe ser un texto'
}),
Vigente: z.boolean({
required_error: 'Falta el estado de vigencia',
invalid_type_error: 'El estado de vigencia debe ser un booleano'
})
});
export const ActividadesArrayResponseSchema = ZodHelper.createArraySchema(
ActividadesResponseSchema,
'El array de actividades es requerido'
);
export type TActividadesResponse = z.infer<typeof ActividadesResponseSchema>;
export type TActividadesStorage = z.infer<typeof ActividadesResponseSchema>; Este esquema permite validar tanto un elemento individual como un array completo, descartando elementos que no coincidan con la estructura esperada.
Integración con modelos: datos más consistentes
Un modelo en Angular puede apoyarse en Zod para asegurar que las instancias se creen de forma correcta. Esto reduce errores inesperados y facilita el control sobre qué datos entran al sistema.
Modelo Actividad
import { ZodHelper } from 'src/app/core/helper/zod-helper/zod.helper';
import { TextHelper } from 'src/app/core/helper/text-helper/text.helper';
import { ActividadesResponseSchema, TActividadesResponse, TActividadesStorage } from './actividades.schema';
export class Actividades {
constructor(
public id: number,
public codigo: string,
public descripcion: string,
public tipo: string,
public vigente: boolean
) {}
/** Crea una instancia de Actividades a partir de un objeto JSON. */
static fromJson(json: TActividadesResponse): Actividades {
try {
const parsed = ZodHelper.safeParseOrThrow(ActividadesResponseSchema, json, 'Error al parsear Actividades');
return new Actividades(parsed.Id, parsed.Codigo, TextHelper.toTitleCase(parsed.Descripcion), parsed.Tipo, parsed.Vigente);
} catch (error) {
console.error(`Error al parsear Actividades: ${error}`);
throw error;
}
}
/**
* Crea un array de Actividades a partir de un array JSON.
* Filtra los elementos que no cumplan con el schema sin interrumpir la ejecución.
*/
static createArrayFromJson(json: TActividadesResponse[]): Actividades[] {
if (!Array.isArray(json)) {
console.error('El parámetro no es un array');
return [];
}
return json
.map((item) => {
try {
// Intentamos validar cada elemento
const parsed = ZodHelper.safeParseOrNull(ActividadesResponseSchema, item, 'Error al parsear actividades en array');
// Si la validación falló, retornamos null
if (!parsed) return null;
return Actividades.fromJson(parsed);
} catch (error) {
console.error(`Error al procesar elemento del array: ${error}`);
return null;
}
})
// Filtrar elementos null y asegurar que el resultado sea Actividades[]
.filter((actividades): actividades is Actividades => actividades !== null);
}
/** Genera una instancia de Actividades desde un json de storage */
static fromStorage(json: TActividadesResponse): Actividades {
const parsed = ZodHelper.safeParseOrThrow(ActividadesResponseSchema, json, 'Error al parsear Actividad desde storage');
return new Actividades(parsed.Id, parsed.Codigo, TextHelper.toTitleCase(parsed.Descripcion), parsed.Tipo, parsed.Vigente);
}
} Este enfoque centraliza la validación en el punto de entrada del modelo. Si los datos no cumplen el schema, el error aparece inmediatamente, en lugar de propagarse a través de componentes o servicios.
Validar en runtime no solo previene errores: documenta el contrato de datos y hace que el código sea más predecible en aplicaciones grandes.
Validación en el flujo de trabajo
Cuando me sumé al proyecto en Mapplics, Zod ya formaba parte del flujo de trabajo. Con el tiempo fui entendiendo cómo ayudaba a identificar datos con inconsistencias en la estructura antes de que generaran errores en otras partes del sistema. Ese tipo de validaciones redujo varios casos de debugging innecesario y aportó mayor previsibilidad al manejo de información.
Desde entonces lo incorporo en otros proyectos cuando el contexto lo justifica, especialmente cuando trabajo con APIs externas o estructuras de datos que pueden variar. En escenarios donde intervienen múltiples servicios y modelos dentro de una aplicación Angular grande, aporta una capa de control útil sobre los datos que se manejan en el sistema. Esa verificación en runtime contribuye a mantener un comportamiento más predecible y facilita el mantenimiento general.