Da JSON a Zod
Genera schemi Zod dal JSON, ciascuno con il suo tipo z.infer: elementi di array uniti, chiavi opzionali e nullable marcate, niente indovinato dal campione.
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>
Sintassi di Zod 4, valida anche su Zod 3. z.object scarta ogni chiave che non elenca, quindi un campo assente dal tuo campione sparisce senza avvisi dai dati che analizza.
In ogni posizione qui sotto il tuo campione conteneva solo null, quindi lo schema lì non accetta nient’altro.
Posizioni: 1
Customer.phone
In ogni posizione qui sotto ogni numero era intero. z.number() accetta anche 7.5, e .int() richiederebbe numeri interi: aggiungilo tu solo dove sai che un valore deve essere intero.
Posizioni: 1
LineItem.quantity
In ogni posizione qui sotto non è stato possibile dedurre nulla, quindi lo schema non controlla ciò che c’è lì: z.unknown() accetta qualsiasi valore e z.record(z.string(), z.unknown()) qualsiasi oggetto.
Posizioni: 1
Root.tags[]
Un controllo che gira ogni volta che arrivano i dati
Un tipo TypeScript viene controllato quando il tuo codice viene compilato e non c’è più quando il codice gira. Uno schema Zod è la parte che resta: gira dentro il tuo programma e controlla ogni payload man mano che arriva — una risposta di un’API, il corpo di un webhook, un messaggio preso da una coda — prima che il tuo codice ci faccia affidamento. Incolla un campione di quel JSON e questa pagina scrive gli schemi per te, pronti da incollare in un modulo: ogni oggetto che ha delle chiavi diventa uno "z.object" con nome, gli elementi di un array uniti in uno solo, e ogni schema è seguito dal tipo TypeScript che produce.
È la risposta che la pagina Da JSON a TypeScript dà per lo stesso JSON, scritta come un validatore invece che come tipi. Le due pagine leggono un’unica inferenza, quindi quali chiavi sono opzionali, quali valori diventano una union, dove null viene tenuto distinto da una chiave mancante e come si chiama ogni oggetto annidato vengono decisi una volta e soltanto scritti due volte. Quelle regole di unione sono l’argomento della guida della pagina Da JSON a TypeScript e qui non vengono raccontate di nuovo. Questa guida parla di ciò che lo schema fa con i dati veri una volta che gira, e di ciò che lascia decidere a te.
Ciò che passa, e ciò che viene rifiutato
Uno schema di questa pagina accetta il campione da cui è stato scritto, su Zod 3 come su Zod 4, tranne nell’unico caso che ha un avviso proprio, un numero infinito su Zod 4. Oltre quel campione accetta qualunque cosa resti dentro ciò che lo schema dice, il che, per i dati che riceverai davvero, funziona così:
- Al di qua del limite di profondità, una chiave scritta senza ".optional()" è obbligatoria. Un payload che la omette viene rifiutato, e lo stesso vale per uno che ci mette un genere di valore che lo schema non nomina — una stringa dove dice "z.number()", un oggetto dove dice "z.string()".
- Una chiave scritta con ".optional()" può mancare, e una scritta con ".nullable()" può contenere null. Nessuna delle due lascia passare un altro genere di valore, quindi una chiave opzionale presente deve comunque contenere ciò che lo schema nomina.
- Uno "z.union" accetta uno qualsiasi dei suoi membri e nient’altro. Uno "z.array" accetta un numero qualsiasi di elementi, anche nessuno del tutto, purché ciascuno corrisponda allo schema scritto per i suoi elementi.
- Dove il campione non ha mostrato nulla — gli elementi di un array vuoto, un oggetto senza chiavi, un valore annidato oltre il limite di profondità — lo schema non controlla ciò che c’è: accetta qualsiasi valore, o qualsiasi oggetto dove l’oggetto del campione non aveva chiavi, e un avviso dice dove.
Una chiave che lo schema non elenca viene lasciata passare e poi scartata dal risultato; è lo scarto, e ha una sezione propria. Una chiave resta fuori da tutto questo. Una chiave chiamata "__proto__" viene scritta come chiave calcolata, tra parentesi quadre, perché scritta così com’è imposterebbe il prototipo dell’oggetto in cui si trova invece di nominare una chiave — eppure nessuna delle due versioni di Zod riporta quella chiave in ciò che un’analisi restituisce, e Zod 4 non ne controlla affatto il valore.
Perché lo schema non si stringe mai da solo
Ogni campione ha cose in comune che non può dimostrare: ogni id un numero intero, ogni indirizzo email con la forma di un indirizzo email, un ruolo che non ha mai detto altro che admin. Questa è una regolarità, e una regolarità non è un controllo. Un generatore è tentato di scriverne una comunque — ".int()" sugli id, "z.email()" sugli indirizzi, un letterale sul ruolo — e questo non lo fa mai, perché un campione mostra ciò che i tuoi dati possono contenere e mai ciò che devono contenere. Un tipo più stretto dei tuoi dati costa un errore di compilazione sulla tua stessa macchina. Uno schema più stretto dei tuoi dati costa una richiesta rifiutata in produzione: il primo ruolo che non è admin, il primo id che vale 7.5, il primo indirizzo che il pattern non aveva previsto.
Anche i controlli ti cambierebbero sotto i piedi. Lo "z.uuid()" di Zod 4 rifiuta stringhe con la forma di un UUID che il ".uuid()" di Zod 3 accettava, perché controlla i bit di variante, e il ".int()" di Zod 4 rifiuta un intero fuori dall’intervallo sicuro che quello di Zod 3 lasciava passare — quindi un controllo indovinato dal campione di oggi sarebbe un controllo diverso a seconda dello Zod che installi. Nulla di ciò che il campione si limita a suggerire viene dunque scritto nello schema. Dove conta quando arrivano i dati, la pagina te lo dice invece in un avviso, e la modifica è lasciata a te.
Ciò che lo schema non dirà da sé
Sotto lo schema, la pagina elenca ciò che ha notato e non ha scritto dentro: un avviso per ogni genere qui sotto che si applica, dato una volta con ogni posizione per cui vale. Una posizione si scrive come l’output nomina le cose — "Customer.phone" per una chiave, "Root.tags[]" per gli elementi di un array, la chiave tra virgolette e parentesi quadre dove non è un identificatore e per "__proto__" — così la si può trovare nello schema a colpo d’occhio. L’esempio che si carica con la pagina mostra ogni genere tranne quello su Infinity.
- Solo null. Il campione non ha mai contenuto altro che null in quella posizione, quindi lo schema dice "z.null()" e rifiuta il primo valore vero. Decidi che cosa contiene il campo quando viene riempito e scrivilo tu — "z.string().nullable()", diciamo — oppure incolla un campione in cui ha un valore.
- Infinity. Un numero che supera ciò che un numero JavaScript può contenere, come 1e999, diventa Infinity o -Infinity quando il JSON viene analizzato. Lo "z.number()" di Zod 4 rifiuta un numero infinito e quello di Zod 3 lo accetta, per cui su Zod 4 questo è l’unico caso in cui uno schema rifiuta proprio il campione da cui è stato fatto. La domanda che solleva riguarda i dati anziché lo schema: le cifre vanno perse prima che un qualsiasi validatore le veda, quindi spetta a ciò che l’ha scritto rispondere se quel valore debba proprio essere un numero.
- Numeri interi. Ogni numero in quella posizione era intero, e "z.number()" accetta anche 7.5. Dove un valore deve essere intero — un id, un conteggio, una quantità — aggiungi ".int()" tu; dove è un prezzo che per caso era tondo, lascialo stare. L’avviso viene omesso per qualunque posizione contenga un intero fuori dall’intervallo sicuro, da "Number.MIN_SAFE_INTEGER" a "Number.MAX_SAFE_INTEGER", perché il ".int()" di Zod 4 li rifiuta, e un consiglio che farebbe rifiutare a uno schema il proprio campione è l’unico genere che la pagina non darà.
- Nulla da dedurre. Gli elementi di un array vuoto, un oggetto senza chiavi e un valore annidato a più di 100 livelli di profondità non danno all’inferenza nulla su cui basarsi, quindi lo schema non controlla ciò che c’è: "z.unknown()" accetta qualsiasi valore in assoluto, e "z.record(z.string(), z.unknown())" qualsiasi oggetto. Incolla un campione in cui quell’array ha elementi e quell’oggetto ha chiavi, oppure scrivi a mano quella parte dello schema.
Un avviso non cambia mai nemmeno un byte dello schema. È una frase accanto a esso, nella lingua della pagina, e la modifica che indica spetta a te farla o saltarla. Non c’è alcun avviso su formati di stringa, letterali o enumerazioni: ciascuno equivarrebbe a indovinare una regola che il campione non può mostrare.
Le chiavi che lo schema non elenca vengono scartate
"z.object" lascia passare un oggetto con chiavi che non elenca e le scarta da ciò che restituisce. È il comportamento predefinito di Zod su entrambe le versioni, e la cosa più silenziosa che uno schema faccia: un campo che al tuo campione è capitato di non avere sparisce dai dati che riceve il tuo codice, senza alcun errore a segnalarlo. È per questo che la frase sotto ogni schema di questa pagina lo menziona.
La pagina non sceglie per te tra rigoroso e permissivo, perché ciascuno afferma più di quanto un campione possa mostrare. Un oggetto rigoroso rifiuta qualsiasi chiave che non elenca — nessuna chiave oltre a queste — e nessun campione può dimostrarlo per il prossimo payload. Un oggetto permissivo tiene le chiavi in più, e il suo tipo dedotto acquista una firma di indice per esse, quindi smetterebbe di essere la risposta della pagina TypeScript. Lo scarto è l’unico comportamento che accetta ciò che accetta il tipo TypeScript e deduce comunque quel tipo — un valore con proprietà in più soddisfa anche un’interfaccia. Per scegliere diversamente, modifica lo schema a mano:
- Per rifiutare le chiavi che lo schema non elenca, scrivi "z.strictObject" dove l’output scrive "z.object" su Zod 4, oppure concatena ".strict()" allo "z.object" su Zod 3.
- Per tenerle, scrivi "z.looseObject" su Zod 4, oppure concatena ".passthrough()" su Zod 3. Zod 4 esegue ancora entrambi quei metodi di Zod 3, e li definisce legacy.
Ogni oggetto dell’output è uno schema a sé, quindi la scelta si fa un oggetto alla volta: rendere rigorosa la radice non cambia nulla degli oggetti annidati al suo interno, il che spesso è ciò che vuoi quando solo sull’involucro esterno spetta a te insistere.
Uno stesso nome per ogni schema e per il suo tipo
Ogni schema è seguito dal suo tipo — "export type Customer = z.infer<typeof Customer>" subito dopo "export const Customer" —, che è il modo in cui zod.dev scrive i propri esempi: uno stesso nome per il valore che controlla i dati e per il tipo che produce, dato che TypeScript tiene valori e tipi in spazi dei nomi separati. Quel tipo è quello che la pagina Da JSON a TypeScript stampa per lo stesso JSON, nome per nome e chiave per chiave, con le stesse chiavi opzionali, le stesse union e null negli stessi punti — salvo le eccezioni qui sotto.
Il repository controlla quella promessa invece di fidarsene: un corpus di campioni passa per entrambe le pagine e poi per il compilatore TypeScript con ciascuna versione di Zod, compilatore al quale si chiede, per ogni nome, se i due tipi sono identici e se ciascuno è assegnabile all’altro. Dove risponde altrimenti, il punto è in profondità in un documento. Oltre il limite di profondità, dove una chiave contiene "z.unknown()", Zod 3 deduce quella chiave come opzionale mentre la pagina TypeScript la rende obbligatoria. E su Zod 4 il compilatore rinuncia al tipo di un array annidato a decine di livelli di profondità, segnalando l’errore TS2589 sulla riga stessa del tipo; lo schema sopra quella riga gira ancora e accetta ancora il campione, e si perde soltanto il tipo dedotto.
Entrambi i confronti leggono una chiave opzionale come fa TypeScript per impostazione predefinita. Con "exactOptionalPropertyTypes", che è disattivato a meno che un progetto non lo attivi, lo "z.infer" di una chiave opzionale ammette anche un undefined esplicito, su entrambe le versioni di Zod, dove il tipo della pagina TypeScript non lo ammette.
Scritto per Zod 4, e ancora valido su Zod 3
L’output si attiene a ciò che hanno entrambe le versioni principali — "z.object", "z.array", "z.union" su due o più membri, "z.string()", "z.number()", "z.boolean()", "z.null()", "z.unknown()", "z.record(z.string(), z.unknown())", ".optional()", ".nullable()" e "z.infer" — sotto la riga di import con cui si aprono gli esempi di zod.dev stesso. Nulla di esso è arrivato con Zod 4 e nulla vi è deprecato, quindi un progetto che non ha lasciato Zod 3 può incollarlo così com’è.
Lo stesso testo però non si comporta in modo identico su entrambe, e ogni differenza è detta in questa guida dove conta. Lo "z.number()" di Zod 4 rifiuta un numero infinito dove quello di Zod 3 lo accetta, il che è l’avviso Infinity. Una chiave il cui valore è "z.unknown()" è opzionale nel tipo dedotto di Zod 3 e obbligatoria in quello di Zod 4 — e obbligatoria anche quando i dati vengono analizzati, a partire da Zod 4.4 —, cosa che questo output non scrive mai se non oltre il limite di profondità. Zod 3 controlla una chiave chiamata "__proto__" e Zod 4 no. E il compilatore rinuncia al tipo di Zod 4 per un array annidato a decine di livelli di profondità, dove ricava quello di Zod 3.
È scritto per Zod normale, con i metodi: "z.string().nullable().optional()". Zod Mini scrive lo stesso schema con funzioni invece, "z.optional(z.nullable(z.string()))", quindi Zod Mini non può eseguire l’output così com’è.
Perché la radice viene per ultima
La pagina TypeScript stampa la radice per prima e gli oggetti che usa dopo di essa, poiché un tipo può essere usato prima della riga che lo dichiara. Uno schema non può: è un valore, e un "const" letto sopra la propria dichiarazione lancia un ReferenceError mentre il modulo è ancora in caricamento. Così ogni schema qui viene dopo ciascuno schema che usa, e la radice viene per ultima — Customer e LineItem per primi nell’esempio, poi il Root che li contiene —, il che spiega perché la radice che apre la pagina TypeScript chiude questa.
Un ordine del genere esiste sempre. L’inferenza è un albero, e ogni oggetto che estrae è usato da esattamente un punto, quindi nessuno schema ha bisogno di riferirsi a se stesso o a uno stampato dopo, e l’output non ha mai bisogno di "z.lazy".
Domande frequenti
- Perché un id è "z.number()" anziché "z.number().int()"?
- Perché un campione può mostrare che finora ogni id era intero, ma non che lo sarà il prossimo. La pagina lo dice invece: l’avviso sui numeri interi elenca ogni posizione in cui ogni numero era intero, e dove sai che un valore deve restare intero, aggiungere ".int()" è una modifica di una parola. L’avviso è omesso dove un numero è un intero fuori dall’intervallo sicuro, dato che il ".int()" di Zod 4 rifiuterebbe quello stesso campione.
- Perché un indirizzo email esce come semplice "z.string()"?
- Perché una stringa che nel tuo campione sembra un’email non dice nulla sulla prossima, e un controllo di formato corretto deve seguire i pattern di Zod stesso, che cambiano da una versione all’altra — lo "z.uuid()" di Zod 4 rifiuta già stringhe che il ".uuid()" di Zod 3 lasciava passare. Date, URL e UUID restano stringhe per lo stesso motivo, e un campo che non ha mai contenuto altro che qualche valore non diventa mai un’enumerazione o un letterale. Se conosci la regola, inseriscila; lo schema è semplice codice tuo.
- Perché il mio stesso campione fallisce su Zod 4?
- Contiene un numero che supera ciò che un numero JavaScript può contenere — 1e999, diciamo —, che è diventato Infinity o -Infinity quando il JSON è stato analizzato, e lo "z.number()" di Zod 4 rifiuta un numero infinito dove quello di Zod 3 lo accetta. L’avviso Infinity nomina ogni posizione coinvolta. A parte questo, uno schema accetta sempre il campione da cui è nato, dato che ogni valore del campione vi è entrato, e il repository lo controlla su entrambe le versioni con un corpus di campioni.
- Perché un campo che ho inviato è sparito dal risultato analizzato?
- Perché lo schema non lo elenca. "z.object" accetta un oggetto con chiavi in più e lo restituisce senza di esse, su entrambe le versioni, e una chiave che mancava al tuo campione è una chiave che lo schema non ha mai imparato. Aggiungila allo schema, oppure rendi permissivo quell’unico oggetto — "z.looseObject" su Zod 4, ".passthrough()" su Zod 3 — se le chiavi sconosciute devono passare intatte.
- Ho spostato uno schema sotto un altro e ho ottenuto un ReferenceError. Perché?
- Perché uno schema è un valore, e JavaScript non lascia leggere un valore sopra la riga che lo definisce. La pagina stampa ogni schema dopo tutti gli schemi che usa, con la radice per ultima, esattamente per questo motivo; tieni uno schema sopra tutto ciò che vi fa riferimento e l’errore sparisce.
- Perché lo schema si chiama Customer e non CustomerSchema?
- È la convenzione di zod.dev stesso: uno schema e il tipo che deduce condividono uno stesso nome, cosa che TypeScript consente perché valori e tipi vivono in spazi dei nomi separati. Il nome in sé è quello che la pagina Da JSON a TypeScript dà allo stesso oggetto, quindi è la stessa parola su entrambe le pagine, per lo schema come per il suo tipo.
- Per quale versione di Zod è scritto l’output?
- Zod 4, senza usare nulla che manchi a Zod 3, quindi gira senza modifiche su entrambe. Le due differiscono in qualche punto — un numero infinito, una chiave oltre il limite di profondità, una chiave chiamata "__proto__" e il tipo di un array annidato molto in profondità — e ciascuno è spiegato sopra. È Zod normale con metodi concatenati; Zod Mini scrive lo stesso schema con funzioni e non può eseguirlo così com’è.
- Posso incollare dati veri, credenziali comprese?
- Sì. Lo schema viene elaborato nel tuo browser: ciò che incolli viene letto sulla tua stessa macchina e non viene inviato a un server, memorizzato o registrato. Inoltre lo schema non contiene nessuno dei tuoi valori, solo le tue chiavi e il genere di valore sotto ciascuna, quindi un token nel campione esce come "z.string()" e nient’altro.
Strumenti correlati
- Da JSON a TypeScript
Uno schema di questa pagina gira come parte del tuo codice e controlla i dati ogni volta che arrivano. Quella pagina dà la stessa risposta per lo stesso JSON sotto forma di semplici tipi TypeScript, con gli stessi nomi, che vengono controllati quando il tuo codice viene compilato e non aggiungono nulla in fase di esecuzione.
- Da JSON a Go
Questa pagina lascia a te il compito di richiedere numeri interi dove ogni numero del tuo campione era intero. Quella pagina ricava la stessa forma con gli stessi nomi di tipo, ma Go non ha un tipo che sia semplicemente un numero JSON, quindi dove ogni numero è scritto come numero intero dà al campo un tipo intero, e un valore scritto con un punto decimale non verrà decodificato lì.
- Tester JSONPath
Testa query JSONPath (RFC 9535) su JSON.
- Generatore di tabelle Markdown
Crea e allinea tabelle Markdown da CSV, TSV o JSON.