JSON en Zod
Génère des schémas Zod depuis JSON, chacun avec son type z.infer : éléments de tableau fusionnés, clés optionnelles et nullable marquées, rien de deviné.
import * as z from "zod"
export const Customer = z.object({
name: z.string(),
email: z.string(),
phone: z.null(),
})
export type Customer = z.infer<typeof Customer>
export const LineItem = z.object({
sku: z.string(),
quantity: z.number(),
price: z.number(),
note: z.string().nullable(),
backordered: z.boolean().optional(),
})
export type LineItem = z.infer<typeof LineItem>
export const Root = z.object({
id: z.string(),
customer: Customer,
lineItems: z.array(LineItem),
tags: z.array(z.unknown()),
})
export type Root = z.infer<typeof Root>
Syntaxe Zod 4, valide aussi avec Zod 3. z.object écarte toute clé qu’il ne liste pas : un champ absent de votre exemple disparaît sans avertissement des données qu’il analyse.
À chaque position ci-dessous, votre exemple ne contenait que null, donc le schéma n’y accepte rien d’autre.
Positions : 1
Customer.phone
À chaque position ci-dessous, chaque nombre était entier. z.number() accepte aussi 7.5, et .int() exigerait des nombres entiers : ajoutez-le vous-même, seulement là où vous savez qu’une valeur doit être entière.
Positions : 1
LineItem.quantity
À chaque position ci-dessous, rien n’a pu être déduit, donc le schéma ne vérifie pas ce qui s’y trouve : z.unknown() accepte n’importe quelle valeur, et z.record(z.string(), z.unknown()) n’importe quel objet.
Positions : 1
Root.tags[]
Une vérification qui s’exécute chaque fois que les données arrivent
Un type TypeScript est vérifié quand votre code se compile et a disparu au moment où il s’exécute. Un schéma Zod est la partie qui reste : il s’exécute dans votre programme et vérifie chaque charge utile à son arrivée — une réponse d’API, le corps d’un webhook, un message sorti d’une file d’attente — avant que votre code ne s’y fie. Collez un exemple de ce JSON et cette page écrit les schémas pour vous, prêts à coller dans un module : chaque objet qui a des clés devient un "z.object" nommé, les éléments d’un tableau fusionnés en un seul, et chaque schéma est suivi du type TypeScript qu’il produit.
C’est la réponse que la page JSON en TypeScript donne pour le même JSON, écrite sous forme de validateur plutôt que de types. Les deux pages lisent une seule inférence : quelles clés sont optionnelles, quelles valeurs deviennent une union, où null est séparé d’une clé absente et comment s’appelle chaque objet imbriqué sont donc décidés une fois et seulement écrits deux fois. Ces règles de fusion sont le sujet du guide de la page JSON en TypeScript et ne sont pas racontées de nouveau ici. Ce guide porte sur ce que fait le schéma avec des données réelles une fois qu’il s’exécute, et sur ce qu’il vous laisse décider.
Ce qui passe, et ce qui est rejeté
Un schéma de cette page accepte l’exemple à partir duquel il a été écrit, sur Zod 3 comme sur Zod 4, sauf dans le seul cas qui a son propre avis, un nombre infini sur Zod 4. Au-delà de cet exemple, il accepte tout ce qui reste à l’intérieur de ce que dit le schéma, ce qui donne ceci pour les données que vous recevrez réellement :
- En deçà de la limite de profondeur, une clé écrite sans ".optional()" est obligatoire. Une charge utile qui l’omet est rejetée, tout comme une charge utile qui y place une valeur d’une nature que le schéma ne nomme pas — une chaîne là où il dit "z.number()", un objet là où il dit "z.string()".
- Une clé écrite avec ".optional()" peut être absente, et une clé écrite avec ".nullable()" peut contenir null. Aucune des deux ne laisse passer une autre nature de valeur, si bien qu’une clé optionnelle présente doit quand même contenir ce que nomme le schéma.
- Un "z.union" accepte n’importe lequel de ses membres et rien d’autre. Un "z.array" accepte n’importe quel nombre d’éléments, y compris aucun du tout, pourvu que chacun corresponde au schéma écrit pour ses éléments.
- Là où l’exemple n’a rien montré — les éléments d’un tableau vide, un objet sans clés, une valeur imbriquée au-delà de la limite de profondeur — le schéma ne vérifie pas ce qui s’y trouve : il accepte n’importe quelle valeur, ou n’importe quel objet là où l’objet de l’exemple n’avait pas de clés, et un avis dit où.
Une clé que le schéma ne liste pas est laissée passer, puis écartée du résultat ; c’est la mise à l’écart, et elle a sa propre section. Une clé reste en dehors de tout cela. Une clé nommée "__proto__" s’écrit comme une clé calculée, entre crochets, parce qu’écrite telle quelle elle définirait le prototype de l’objet où elle se trouve au lieu de nommer une clé — et pourtant aucune des deux versions de Zod ne rend cette clé dans ce que renvoie une analyse, et Zod 4 ne vérifie pas du tout sa valeur.
Pourquoi le schéma ne se resserre jamais de lui-même
Tout exemple a des points communs qu’il ne peut pas prouver : chaque id étant un nombre entier, chaque adresse de courriel ayant la forme d’une adresse de courriel, un rôle qui n’a jamais dit autre chose qu’admin. C’est une régularité, et une régularité n’est pas une vérification. Un générateur est tenté d’en écrire une quand même — ".int()" sur les id, "z.email()" sur les adresses, un littéral sur le rôle — et celui-ci ne le fait jamais, parce qu’un exemple montre ce que vos données peuvent contenir et jamais ce qu’elles doivent contenir. Un type plus étroit que vos données coûte une erreur de compilation sur votre propre machine. Un schéma plus étroit que vos données coûte une requête rejetée en production : le premier rôle qui n’est pas admin, le premier id valant 7.5, la première adresse que le motif n’avait pas prévue.
Les vérifications bougeraient aussi sous vos pieds. Le "z.uuid()" de Zod 4 rejette des chaînes en forme d’UUID que le ".uuid()" de Zod 3 acceptait, parce qu’il vérifie les bits de variante, et le ".int()" de Zod 4 rejette un entier hors de la plage sûre que celui de Zod 3 laissait passer — si bien qu’une vérification devinée à partir de l’exemple d’aujourd’hui serait une vérification différente selon le Zod que vous installez. Rien de ce que l’exemple ne fait que suggérer n’est donc écrit dans le schéma. Là où cela compte à l’arrivée des données, la page vous le dit plutôt dans un avis, et la modification vous est laissée.
Ce que le schéma ne dira pas de lui-même
Sous le schéma, la page liste ce qu’elle a remarqué sans l’y écrire : un avis pour chaque sorte ci-dessous qui s’applique, donné une fois avec chaque position où il vaut. Une position s’écrit comme la sortie nomme les choses — "Customer.phone" pour une clé, "Root.tags[]" pour les éléments d’un tableau, la clé entre guillemets et crochets là où ce n’est pas un identifiant et pour "__proto__" — de sorte qu’on peut la retrouver d’un coup d’œil dans le schéma. L’exemple que charge la page montre toutes les sortes sauf celle qui concerne Infinity.
- Uniquement null. L’exemple n’a jamais rien contenu d’autre que null à cette position, donc le schéma dit "z.null()" et rejette la première vraie valeur. Décidez ce que contient le champ quand il est rempli et écrivez-le vous-même — "z.string().nullable()", disons — ou collez un exemple dans lequel il a une valeur.
- Infinity. Un nombre qui dépasse ce qu’un nombre JavaScript peut contenir, comme 1e999, devient Infinity ou -Infinity quand le JSON est analysé. Le "z.number()" de Zod 4 rejette un nombre infini et celui de Zod 3 l’accepte, si bien que sur Zod 4 c’est le seul cas où un schéma rejette l’exemple même à partir duquel il a été fait. La question que cela soulève porte sur les données plutôt que sur le schéma : les chiffres sont perdus avant qu’aucun validateur ne les voie, donc savoir si cette valeur devrait même être un nombre, c’est à ce qui l’a écrite d’y répondre.
- Nombres entiers. Chaque nombre à cette position était entier, et "z.number()" accepte aussi 7.5. Là où une valeur doit être entière — un id, un décompte, une quantité — ajoutez ".int()" vous-même ; là où c’est un prix qui s’est trouvé être rond, n’y touchez pas. L’avis est omis pour toute position qui contient un entier hors de la plage sûre, de "Number.MIN_SAFE_INTEGER" à "Number.MAX_SAFE_INTEGER", parce que le ".int()" de Zod 4 rejette ceux-là, et un conseil qui ferait rejeter à un schéma son propre exemple est la seule sorte que la page ne donnera pas.
- Rien à déduire. Les éléments d’un tableau vide, un objet sans clés et une valeur imbriquée sur plus de 100 niveaux ne donnent à l’inférence rien sur quoi s’appuyer, donc le schéma ne vérifie pas ce qui s’y trouve : "z.unknown()" accepte absolument n’importe quelle valeur, et "z.record(z.string(), z.unknown())" n’importe quel objet. Collez un exemple dans lequel ce tableau a des éléments et cet objet a des clés, ou écrivez cette partie du schéma à la main.
Un avis ne change jamais le moindre octet du schéma. C’est une phrase à côté de lui, dans la langue de la page, et la modification qu’il indique, c’est à vous de la faire ou de ne pas la faire. Il n’y a pas d’avis sur les formats de chaîne, les littéraux ou les énumérations : chacun reviendrait à deviner une règle que l’exemple ne peut pas montrer.
Les clés que le schéma ne liste pas sont écartées
"z.object" laisse passer un objet portant des clés qu’il ne liste pas et les écarte de ce qu’il renvoie. C’est le comportement par défaut de Zod sur les deux versions, et la chose la plus discrète que fasse un schéma : un champ qui manquait par hasard à votre exemple disparaît des données que reçoit votre code, sans qu’aucune erreur le signale. C’est pourquoi la phrase sous chaque schéma de cette page le mentionne.
La page ne choisit pas pour vous entre strict et permissif, parce que chacun affirme plus qu’un exemple ne peut montrer. Un objet strict rejette toute clé qu’il ne liste pas — aucune clé en dehors de celles-ci — et aucun exemple ne peut prouver cela de la prochaine charge utile. Un objet permissif garde les clés en trop, et son type déduit gagne une signature d’index pour elles, si bien qu’il cesserait d’être la réponse de la page TypeScript. La mise à l’écart est le seul comportement qui accepte ce qu’accepte le type TypeScript et déduit quand même ce type — une valeur qui a des propriétés en plus satisfait aussi une interface. Pour choisir autrement, modifiez le schéma à la main :
- Pour rejeter les clés que le schéma ne liste pas, écrivez "z.strictObject" là où la sortie écrit "z.object" sur Zod 4, ou chaînez ".strict()" au "z.object" sur Zod 3.
- Pour les garder, écrivez "z.looseObject" sur Zod 4, ou chaînez ".passthrough()" sur Zod 3. Zod 4 exécute toujours ces deux méthodes de Zod 3, et les qualifie d’héritées.
Chaque objet de la sortie est un schéma à part entière, donc le choix se fait objet par objet : rendre la racine stricte ne change rien aux objets imbriqués à l’intérieur, ce qui est souvent ce que vous voulez quand ce n’est que sur l’enveloppe extérieure que vous êtes en droit d’insister.
Un même nom pour chaque schéma et son type
Chaque schéma est suivi de son type — "export type Customer = z.infer<typeof Customer>" juste après "export const Customer" —, soit la façon dont zod.dev écrit ses propres exemples : un même nom pour la valeur qui vérifie les données et pour le type qu’elle produit, puisque TypeScript range les valeurs et les types dans des espaces de noms séparés. Ce type est celui qu’affiche la page JSON en TypeScript pour le même JSON, nom pour nom et clé pour clé, avec les mêmes clés optionnelles, les mêmes unions et null aux mêmes endroits — aux exceptions ci-dessous près.
Le dépôt vérifie cette promesse au lieu de s’y fier : un corpus d’exemples passe par les deux pages puis par le compilateur TypeScript avec chaque version de Zod, compilateur auquel on demande, pour chaque nom, si les deux types sont identiques et si chacun est assignable à l’autre. Là où il répond autrement, l’endroit se trouve en profondeur dans un document. Au-delà de la limite de profondeur, là où une clé contient "z.unknown()", Zod 3 déduit cette clé comme optionnelle alors que la page TypeScript la rend obligatoire. Et sur Zod 4 le compilateur renonce au type d’un tableau imbriqué sur des dizaines de niveaux, en signalant l’erreur TS2589 sur la ligne même du type ; le schéma au-dessus de cette ligne s’exécute toujours et accepte toujours l’exemple, et seul le type déduit est perdu.
Les deux comparaisons lisent une clé optionnelle comme TypeScript le fait par défaut. Sous "exactOptionalPropertyTypes", qui est désactivé sauf si un projet l’active, le "z.infer" d’une clé optionnelle admet aussi un undefined explicite, sur l’une ou l’autre version de Zod, là où le type de la page TypeScript ne l’admet pas.
Écrit pour Zod 4, et toujours valide sur Zod 3
La sortie s’en tient à ce que les deux versions majeures ont — "z.object", "z.array", "z.union" sur deux membres ou plus, "z.string()", "z.number()", "z.boolean()", "z.null()", "z.unknown()", "z.record(z.string(), z.unknown())", ".optional()", ".nullable()" et "z.infer" — sous la ligne d’import par laquelle s’ouvrent les propres exemples de zod.dev. Rien de ce qu’elle contient n’est apparu dans Zod 4 ni n’y est obsolète, si bien qu’un projet qui n’a pas quitté Zod 3 peut la coller telle quelle.
Le même texte ne se comporte pourtant pas de façon identique sur les deux, et chaque différence est dite dans ce guide là où elle compte. Le "z.number()" de Zod 4 rejette un nombre infini là où celui de Zod 3 l’accepte, ce qui est l’avis Infinity. Une clé dont la valeur est "z.unknown()" est optionnelle dans le type déduit de Zod 3 et obligatoire dans celui de Zod 4 — et obligatoire aussi quand les données sont analysées, à partir de Zod 4.4 —, ce que cette sortie n’écrit jamais qu’au-delà de la limite de profondeur. Zod 3 vérifie une clé nommée "__proto__" et Zod 4 non. Et le compilateur renonce au type de Zod 4 pour un tableau imbriqué sur des dizaines de niveaux, là où il établit celui de Zod 3.
Elle est écrite pour le Zod ordinaire, avec des méthodes : "z.string().nullable().optional()". Zod Mini écrit le même schéma avec des fonctions à la place, "z.optional(z.nullable(z.string()))", si bien que Zod Mini ne peut pas exécuter la sortie telle qu’elle est.
Pourquoi la racine vient en dernier
La page TypeScript affiche la racine en premier et les objets qu’elle utilise après elle, puisqu’un type peut être utilisé avant la ligne qui le déclare. Un schéma ne le peut pas : c’est une valeur, et un "const" lu au-dessus de sa propre déclaration lève une ReferenceError pendant que le module est encore en cours de chargement. Chaque schéma vient donc ici après chacun des schémas qu’il utilise, et la racine vient en dernier — Customer et LineItem d’abord dans l’exemple, puis le Root qui les contient —, ce qui explique que la racine qui ouvre la page TypeScript ferme celle-ci.
Un tel ordre existe toujours. L’inférence est un arbre, chaque objet qu’elle extrait étant utilisé depuis un seul endroit exactement, si bien qu’aucun schéma n’a besoin de se référer à lui-même ni à un schéma affiché après lui, et la sortie n’a jamais besoin de "z.lazy".
Questions fréquentes
- Pourquoi un id est-il "z.number()" plutôt que "z.number().int()" ?
- Parce qu’un exemple peut montrer que chaque id jusqu’ici était entier, mais pas que le suivant le sera. La page le dit plutôt : l’avis sur les nombres entiers liste chaque position où chaque nombre était entier, et là où vous savez qu’une valeur doit rester entière, ajouter ".int()" est une modification d’un mot. L’avis est omis là où un nombre est un entier hors de la plage sûre, puisque le ".int()" de Zod 4 rejetterait cet exemple lui-même.
- Pourquoi une adresse de courriel ressort-elle en simple "z.string()" ?
- Parce qu’une chaîne qui ressemble à une adresse de courriel dans votre exemple ne dit rien de la suivante, et qu’une vérification de format juste doit suivre les motifs propres à Zod, qui changent d’une version à l’autre — le "z.uuid()" de Zod 4 rejette déjà des chaînes que le ".uuid()" de Zod 3 laissait passer. Les dates, les URL et les UUID restent des chaînes pour la même raison, et un champ qui n’a jamais contenu que quelques valeurs ne devient jamais une énumération ni un littéral. Si vous connaissez la règle, inscrivez-la ; le schéma est du code ordinaire qui vous appartient.
- Pourquoi mon propre exemple échoue-t-il sur Zod 4 ?
- Il contient un nombre qui dépasse ce qu’un nombre JavaScript peut contenir — 1e999, disons —, qui est devenu Infinity ou -Infinity quand le JSON a été analysé, et le "z.number()" de Zod 4 rejette un nombre infini là où celui de Zod 3 l’accepte. L’avis Infinity nomme chaque position concernée. Hormis cela, un schéma accepte toujours l’exemple dont il vient, puisque chaque valeur de l’exemple y est entrée, et le dépôt le vérifie sur les deux versions avec un corpus d’exemples.
- Pourquoi un champ que j’ai envoyé a-t-il disparu du résultat analysé ?
- Parce que le schéma ne le liste pas. "z.object" accepte un objet qui a des clés en trop et le renvoie sans elles, sur l’une ou l’autre version, et une clé qui manquait à votre exemple est une clé que le schéma n’a jamais apprise. Ajoutez-la au schéma, ou rendez cet objet-là permissif — "z.looseObject" sur Zod 4, ".passthrough()" sur Zod 3 — si les clés inconnues doivent passer intactes.
- J’ai déplacé un schéma sous un autre et j’ai obtenu une ReferenceError. Pourquoi ?
- Parce qu’un schéma est une valeur, et que JavaScript ne laisse pas lire une valeur au-dessus de la ligne qui la définit. La page affiche chaque schéma après tous les schémas qu’il utilise, avec la racine en dernier, précisément pour cette raison ; gardez un schéma au-dessus de tout ce qui y fait référence et l’erreur disparaît.
- Pourquoi le schéma s’appelle-t-il Customer et non CustomerSchema ?
- C’est la propre convention de zod.dev : un schéma et le type qu’il déduit partagent un même nom, ce que TypeScript permet parce que les valeurs et les types vivent dans des espaces de noms séparés. Le nom lui-même est celui que la page JSON en TypeScript donne au même objet, si bien que c’est le même mot sur les deux pages, pour le schéma comme pour son type.
- Pour quelle version de Zod la sortie est-elle écrite ?
- Zod 4, sans rien utiliser qui manque à Zod 3, si bien qu’elle s’exécute sans changement sur l’une ou l’autre. Les deux diffèrent en quelques endroits — un nombre infini, une clé au-delà de la limite de profondeur, une clé nommée "__proto__" et le type d’un tableau très profondément imbriqué — et chacun est expliqué plus haut. C’est du Zod ordinaire avec des méthodes chaînées ; Zod Mini écrit le même schéma avec des fonctions et ne peut pas l’exécuter telle qu’elle est.
- Puis-je coller de vraies données, identifiants de connexion compris ?
- Oui. Le schéma est établi dans votre navigateur : ce que vous collez est lu sur votre propre machine et n’est ni envoyé à un serveur, ni stocké, ni journalisé. Le schéma ne contient en outre aucune de vos valeurs, seulement vos clés et la nature de la valeur sous chacune, si bien qu’un jeton dans l’exemple ressort en "z.string()" et rien de plus.
Outils connexes
- JSON en TypeScript
Un schéma produit ici s’exécute avec votre code et vérifie les données chaque fois qu’elles arrivent. Cette page-là donne la même réponse pour le même JSON sous forme de simples types TypeScript, avec les mêmes noms, qui sont vérifiés à la compilation de votre code et n’ajoutent rien à l’exécution.
- JSON en Go
Ici, c’est à vous d’exiger des nombres entiers là où chaque nombre de votre exemple était entier. Cette page-là établit la même forme sous les mêmes noms de types, mais Go n’a pas de type qui soit simplement un nombre JSON, si bien que là où chaque nombre est écrit comme un entier, elle donne au champ un type entier, et une valeur écrite avec un point décimal ne s’y décodera pas.
- Testeur JSONPath
Testez des requêtes JSONPath (RFC 9535) sur du JSON.
- Générateur de tableaux Markdown
Créez et alignez des tableaux Markdown depuis CSV, TSV ou JSON.