Pourquoi Zod ?

Zod est une librairie de validation de schémas TypeScript-first qui fonctionne également en JavaScript pur. Elle permet de valider des données à l'exécution et d'en inférer les types TypeScript automatiquement.

Le problème sans Zod

// Vous recevez des données d'une API — aucune garantie sur la structure
const data = await fetch('/api/user').then(r => r.json());

// Vous supposez que data.name est une string... mais si c'est null ?
const username = data.name.toUpperCase(); // 💥 TypeError potentielle

// Sans validation, les bugs sont silencieux et difficiles à tracer

La solution avec Zod

import { z } from 'zod'; // CommonJS/ESM
// OU via CDN : const { z } = Zod;

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional()
});

// Validation explicite avant utilisation
const user = UserSchema.parse(data); // 🛡️ Lance une erreur si invalide
const username = user.name.toUpperCase(); // ✅ Sûr !

Comparaison avec les alternatives

LibrairieTypeScriptPoidsAPI
Zod✅ natif~14KBChaînage fluide
Joi❌ types séparés~24KBSimilaire
YupPartiel~18KBSimilaire
validator.js~4KBFonctions
CDN (browser) :
<script src="https://cdn.jsdelivr.net/npm/zod/lib/index.umd.js"></script>
<script>
  const { z } = Zod; // Accès au namespace global
</script>

Types primitifs

const { z } = Zod;

// Types de base
z.string()      // Toute chaîne de caractères
z.number()      // Tout nombre (entier ou flottant)
z.boolean()     // true ou false
z.date()        // Objet Date JavaScript
z.null()        // Exactement null
z.undefined()   // Exactement undefined
z.any()         // N'importe quelle valeur (pas de validation)
z.unknown()     // Valeur inconnue — doit être castée avant usage
z.never()       // N'accepte aucune valeur (impossible)

// Literal — valeur exacte
z.literal('admin')     // Exactement la string 'admin'
z.literal(42)          // Exactement le nombre 42
z.literal(true)        // Exactement true

// Enum — liste de valeurs autorisées
const Role = z.enum(['admin', 'user', 'moderator']);
type Role = z.infer<typeof Role>; // 'admin' | 'user' | 'moderator'

Exemples pratiques

// Valider des types simples
z.string().parse('Bonjour');    // ✅ 'Bonjour'
z.number().parse(42);           // ✅ 42
z.boolean().parse(true);        // ✅ true
z.string().parse(42);           // ❌ ZodError: Expected string, received number

// Coercition (conversion automatique)
z.coerce.number().parse('42');  // ✅ 42 (string → number)
z.coerce.date().parse('2024-01-15'); // ✅ Date object

Validateurs

Les validateurs s'appliquent en chaîne sur les types primitifs.

Validateurs string

z.string()
  .min(2, 'Minimum 2 caractères')
  .max(50, 'Maximum 50 caractères')
  .email('Email invalide')
  .url('URL invalide')
  .regex(/^[a-z]+$/, 'Lettres minuscules seulement')
  .startsWith('https://')
  .endsWith('.com')
  .includes('@')
  .length(10)          // Exactement 10 chars
  .trim()              // Supprime les espaces en bordure (transform)
  .toLowerCase()       // Convertit en minuscules (transform)

Validateurs number

z.number()
  .min(0, 'Doit être positif')
  .max(100, 'Maximum 100')
  .int('Doit être un entier')
  .positive()          // > 0
  .nonnegative()       // >= 0
  .negative()          // < 0
  .multipleOf(5)       // Multiple de 5

optional, nullable, default

// .optional() — la valeur peut être undefined
const schema = z.string().optional();
schema.parse(undefined);  // ✅ undefined
schema.parse('hello');    // ✅ 'hello'
schema.parse(null);       // ❌ null n'est pas undefined

// .nullable() — la valeur peut être null
const schema2 = z.string().nullable();
schema2.parse(null);      // ✅ null
schema2.parse(undefined); // ❌

// .default(val) — valeur par défaut si undefined
const schema3 = z.string().default('Inconnu');
schema3.parse(undefined); // ✅ 'Inconnu'

// Combinaisons
z.string().optional().nullable() // string | null | undefined

Objets et tableaux

z.object()

const UserSchema = z.object({
  id:    z.number().int().positive(),
  name:  z.string().min(2).max(50),
  email: z.string().email(),
  role:  z.enum(['admin', 'user']).default('user'),
  bio:   z.string().max(200).optional(),
});

// Inférence de type TypeScript (en TS)
// type User = z.infer<typeof UserSchema>

// Méthodes sur les objets
UserSchema.strict()    // Rejette les propriétés inconnues
UserSchema.partial()   // Rend tous les champs optionnels
UserSchema.required()  // Rend tous les champs obligatoires
UserSchema.extend({ age: z.number() })  // Ajouter des champs
UserSchema.pick({ name: true, email: true })  // Garder seulement name, email
UserSchema.omit({ bio: true })           // Exclure bio

z.array()

// Tableau de strings
z.array(z.string())

// Tableau d'objets
const TagsSchema = z.array(z.string().min(1)).min(1).max(10);

// Validations sur les tableaux
z.array(z.number())
  .min(1, 'Au moins 1 élément')
  .max(100, 'Maximum 100 éléments')
  .nonempty('Tableau vide interdit')

// Premier élément typé différemment (tuple)
z.tuple([z.string(), z.number()])  // [string, number]

Imbrication

const PostSchema = z.object({
  id:     z.number(),
  title:  z.string(),
  author: z.object({           // Objet imbriqué
    id:   z.number(),
    name: z.string(),
  }),
  tags:   z.array(z.string()), // Tableau imbriqué
  meta: z.record(z.string()),  // { [key: string]: string }
});

parse vs safeParse

.parse() — Lance une exception

const UserSchema = z.object({
  name:  z.string().min(2),
  email: z.string().email(),
});

try {
  const user = UserSchema.parse({ name: 'A', email: 'invalide' });
} catch (err) {
  if (err instanceof ZodError) {
    console.log(err.issues);
    // [
    //   { code: 'too_small', path: ['name'], message: 'String must contain at least 2 character(s)' },
    //   { code: 'invalid_string', path: ['email'], message: 'Invalid email' }
    // ]
  }
}

.safeParse() — Retourne un résultat

const result = UserSchema.safeParse({ name: 'A', email: 'invalide' });

if (result.success) {
  // TypeScript sait que result.data est valide ici
  console.log('Données valides :', result.data);
} else {
  // result.error est un ZodError
  console.log('Erreurs :', result.error.issues);
  // Formater les erreurs par champ
  const erreurs = {};
  result.error.issues.forEach(issue => {
    const champ = issue.path.join('.');
    erreurs[champ] = issue.message;
  });
  console.log(erreurs); // { name: '...', email: '...' }
}
Règle : Utilisez safeParse pour valider les entrées utilisateur (formulaires, paramètres URL) car vous pouvez afficher les erreurs sans crasher. Utilisez parse pour des données supposées valides (ex: variables d'environnement).

Transformations & validations avancées

.transform() — Transformer les données

// Normaliser à la validation
const EmailSchema = z.string()
  .email()
  .transform(val => val.toLowerCase().trim());

EmailSchema.parse('  Alice@Example.COM  ');
// ✅ 'alice@example.com'

// Transformer un nombre en string
const StrNumberSchema = z.string().transform(val => parseInt(val, 10));
StrNumberSchema.parse('42'); // ✅ 42

.refine() — Validation personnalisée

// Validation simple
const PasswordSchema = z.string()
  .min(8)
  .refine(val => /[A-Z]/.test(val), 'Doit contenir une majuscule')
  .refine(val => /[0-9]/.test(val), 'Doit contenir un chiffre');

// Validation inter-champs (passwords must match)
const RegisterSchema = z.object({
  password:        z.string().min(8),
  confirmPassword: z.string(),
}).refine(
  data => data.password === data.confirmPassword,
  {
    message: 'Les mots de passe ne correspondent pas',
    path: ['confirmPassword'],  // Champ sur lequel afficher l'erreur
  }
);

z.union() — Types multiples

// Accepter plusieurs types
const IdSchema = z.union([z.string(), z.number()]);
IdSchema.parse('abc-123');  // ✅
IdSchema.parse(42);          // ✅
IdSchema.parse(true);        // ❌

// Union discriminée (plus performant)
const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('login'),  userId: z.string() }),
  z.object({ type: z.literal('logout'), userId: z.string(), duration: z.number() }),
]);

// Messages d'erreur personnalisés
const NameSchema = z.string({
  required_error: 'Le nom est requis',
  invalid_type_error: 'Le nom doit être une chaîne',
}).min(2, 'Trop court').max(50, 'Trop long');
✅ Récapitulatif L04
  • 🛡️ Types primitifs — string, number, boolean, date, literal, enum
  • 🔧 Validateurs — .min(), .max(), .email(), .optional(), .nullable()
  • 📦 Structures — z.object(), z.array(), .strict(), .partial(), .extend()
  • parse vs safeParse — throw vs résultat structuré
  • 🔄 transform + refine — normaliser et valider selon logique métier
→ Exercices L04 → Mini-projet Formulaire