📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~6 min de lecture

Zod parse vs safeParse : lequel choisir en Angular

Tu as adopté Zod pour valider tes données, et maintenant tu hésites à chaque appel : parse() ou safeParse() ? Les deux valident exactement la même chose, avec exactement le même coût. La seule différence, c'est ce qui se passe quand la donnée est invalide. Et ce choix change radicalement la façon dont ton code Angular réagit en production.

Si Zod est encore flou pour toi, commence par les bases dans Installer et utiliser Zod en TypeScript. Ici, on rentre dans le détail de la décision.

La différence en une phrase

  • parse(data) retourne la donnée typée, ou lance une ZodError si elle est invalide.
  • safeParse(data) ne lance jamais. Il retourne un objet discriminé : { success: true, data } ou { success: false, error }.
import { z } from 'zod';

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

// parse : throw
const user = UserSchema.parse(payload); // ZodError si invalide

// safeParse : pas de throw
const result = UserSchema.safeParse(payload);
if (result.success) {
  result.data.email; // typé, sûr
} else {
  result.error.issues; // tableau des problèmes
}

Note : depuis Zod v4, z.email() remplace l'ancien z.string().email(). C'est la version utilisée dans tout le reste de ce guide.

Pourquoi ce n'est pas qu'une histoire de style

Beaucoup de devs choisissent au hasard, puis enrobent parse() d'un try/catch partout. Résultat : du bruit, et des exceptions qui remontent quand on oublie un seul catch. La vraie question à te poser est simple :

Est-ce que cette donnée a une chance réelle d'être invalide à cet endroit, et si oui, est-ce que je veux gérer ce cas ici plutôt que de planter ?

Si la réponse est oui, c'est safeParse. Si la donnée vient d'une source que tu contrôles déjà ou si une donnée invalide est un vrai bug qui doit exploser au plus tôt, c'est parse.

Before / after : sortir du try/catch partout

Le réflexe le plus courant, c'est de tout faire avec parse et d'emballer chaque appel dans un try/catch. Sur un flux RxJS, ça donne vite du code défensif illisible, et il suffit d'oublier un catch pour qu'une exception traverse toute l'app.

// Before : parse + try/catch dispersés, type de retour flou
getUser(id: number): Observable<User | null> {
  return this.#http.get<unknown>(`/api/users/${id}`).pipe(
    map((body) => {
      try {
        return UserSchema.parse(body);
      } catch (error) {
        console.error('Invalid user payload', error);
        return null; // le composant doit maintenant gérer le null partout
      }
    }),
  );
}

La même intention avec safeParse rend le chemin d'erreur explicite et garde un type de retour propre :

// After : safeParse, branchement clair, pas de try/catch
getUser(id: number): Observable<User> {
  return this.#http.get<unknown>(`/api/users/${id}`).pipe(
    map((body) => {
      const result = UserSchema.safeParse(body);
      if (!result.success) {
        throw new ApiContractError('GET /users', result.error);
      }
      return result.data;
    }),
    catchError((error) => this.#reportAndRethrow(error)),
  );
}

Tu n'as plus de try/catch imbriqué, le success documente le cas d'échec, et le type de retour reste User au lieu de User | null qui contaminerait tous les appelants.

Le cas concret : valider une réponse API

Avec HttpClient et safeParse

À la frontière réseau, une API peut renvoyer n'importe quoi : un champ manquant, un type qui change après un déploiement backend, une 200 avec un corps d'erreur. Tu ne veux pas que ça crashe le flux RxJS. safeParse te laisse décider quoi faire.

@Service()
export class UserApi {
  readonly #http = inject(HttpClient);

  getUser(id: number): Observable<User> {
    return this.#http.get<unknown>(`/api/users/${id}`).pipe(
      map((body) => {
        const result = UserSchema.safeParse(body);
        if (!result.success) {
          throw new ApiContractError('GET /users', result.error);
        }
        return result.data;
      }),
    );
  }
}

Ici tu transformes une donnée invalide en une erreur métier explicite, loguée avec le contexte de l'endpoint, au lieu de laisser fuiter une ZodError brute dans toute l'app.

@Service() est le décorateur introduit en Angular 22 : il déclare un singleton root sans providedIn: 'root' et impose inject() (l'injection par constructeur n'est plus possible). Sur Angular 20/21, remplace-le par @Injectable({ providedIn: 'root' }) ; et même en v22, @Injectable reste requis dès que tu sors du singleton root (scope non-root, providers de composant, factory custom).

Avec parse quand tu veux échouer vite

Si une réponse invalide est un contrat cassé qu'aucun composant ne saura récupérer, parse est plus direct. Inutile de brancher sur success pour, de toute façon, relancer une erreur.

getUser(id: number): Observable<User> {
  return this.#http
    .get<unknown>(`/api/users/${id}`)
    .pipe(map((body) => UserSchema.parse(body)));
}

La ZodError remonte dans le flux et tombe naturellement dans le error de ton subscribe ou de ton catchError global. C'est le bon choix quand tu as déjà un handler d'erreur centralisé.

Avec httpResource (Angular v20+)

httpResource accepte une option parse appelée sur la réponse. Elle attend une fonction qui retourne la donnée validée ou lance. C'est donc parse qu'on branche ici, pas safeParse :

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (userResource.value(); as user) {
      <p>{{ user.name }} - {{ user.email }}</p>
    }
    @if (userResource.error()) {
      <p>Données utilisateur invalides.</p>
    }
  `,
})
export class UserCard {
  readonly id = input.required<number>();

  protected readonly userResource = httpResource<User>(
    () => `/api/users/${this.id()}`,
    { parse: UserSchema.parse },
  );
}

Quand parse lance, la resource bascule dans error() et tu l'affiches proprement avec @if. Tu obtiens la gestion gracieuse de safeParse sans écrire un seul try/catch, parce que c'est httpResource qui capture pour toi.

Le cas formulaire : safeParse et le mapping d'erreurs

Sur un formulaire, l'entrée est invalide par nature tant que l'utilisateur tape. Tu ne vas pas lancer une exception à chaque frappe. safeParse est l'évidence, et Zod v4 te donne deux helpers pour transformer l'erreur en quelque chose d'exploitable côté UI :

  • z.flattenError(error) renvoie { formErrors, fieldErrors }, parfait pour mapper sur des FormControl.
  • z.treeifyError(error) renvoie un arbre qui suit la forme de ton schéma, pratique pour les objets imbriqués.
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <form (submit)="submit($event)">
      <!-- ... -->
    </form>
    @for (message of formErrors(); track message) {
      <p class="error">{{ message }}</p>
    }
  `,
})
export class SignUpForm {
  protected readonly formErrors = signal<readonly string[]>([]);

  protected submit(event: SubmitEvent): void {
    event.preventDefault();
    const result = SignUpSchema.safeParse(this.#readFormValue());

    if (!result.success) {
      const { fieldErrors } = z.flattenError(result.error);
      this.formErrors.set(Object.values(fieldErrors).flat());
      return;
    }

    this.#register(result.data);
  }
}

Petite note de migration : si tu viens de Zod v3, tu utilisais sûrement result.error.format() ou result.error.flatten(). Ces méthodes existent toujours en v4 mais sont dépréciées au profit des fonctions z.treeifyError et z.flattenError.

Bonus : passer par ces fonctions top-level rend le code compatible zod/mini tel quel. La version mini ne fournit pas les méthodes de confort error.flatten() / error.format() ; z.flattenError(err) et z.treeifyError(err), elles, fonctionnent dans les deux. Le reste (gain de bundle, API de construction des schémas) est détaillé dans Comment j'ai réduit de 25% mon bundle Angular.

Et l'async ?

Si ton schéma contient une validation asynchrone (un refine qui attend une promesse, par exemple un check d'unicité côté serveur), les versions synchrones lancent une erreur. Utilise alors parseAsync et safeParseAsync, qui ont exactement la même sémantique mais retournent une promesse.

const result = await SignUpSchema.safeParseAsync(value);

Le tableau de décision

Situation parse safeParse
Réponse API que tu veux dégrader proprement x
Réponse API avec handler d'erreur global x
Option parse de httpResource x
Entrée formulaire utilisateur x
Donnée déjà validée à la frontière, usage interne x
Tu dois brancher la logique sur la validité x
Test rapide / fixture que tu contrôles x

La règle qui résume tout : parse pour échouer vite sur ce qui ne devrait jamais arriver, safeParse pour gérer ce qui peut légitimement arriver.

Les pièges à éviter

  1. parse sur de la donnée non fiable sans handler. Une ZodError non capturée remonte jusqu'à un gestionnaire global, ou pire, casse le flux RxJS silencieusement. Si tu n'as pas de catchError, prends safeParse.
  2. Oublier de tester success avant data. Le résultat de safeParse est une union discriminée. TypeScript t'empêche d'accéder à result.data tant que tu n'as pas vérifié result.success : ne contourne pas ce garde-fou avec un as.
  3. Valider deux fois la même donnée. parse et safeParse coûtent pareil, mais valider en boucle sur chaque changement de signal reste du gaspillage. Valide à la frontière, garde le type inféré ensuite. Si tu hésites encore entre any, unknown et Zod pour cette frontière, le détail est dans Arrête d'utiliser any en TypeScript.
  4. Embarquer tout Zod pour trois schémas. Sur un bundle Angular surveillé, regarde zod/mini : même API pour les cas courants, poids réduit. On l'explique dans Comment j'ai réduit de 25% mon bundle Angular.

À retenir

parse et safeParse valident la même chose. Ce que tu choisis, c'est la stratégie d'erreur : exception qui remonte, ou résultat que tu inspectes. En Angular, ça se traduit simplement : safeParse aux endroits où l'invalide est attendu (formulaires, API que tu veux dégrader), parse là où l'invalide est un bug (contrat strict, option parse de httpResource, code interne déjà validé).


Envie de mettre tout ça en pratique sur une vraie archi Angular, de la couche infrastructure aux formulaires ? Découvre les modules d'EasyAngularKit.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.