JSON в Zod
Генерирует схемы Zod из JSON, каждую со своим типом z.infer: элементы массива объединены, необязательные и nullable-ключи отмечены, ничего не угадано.
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>
Синтаксис Zod 4, который действителен и в Zod 3. z.object отбрасывает каждый ключ, которого в нём нет, поэтому поле, отсутствовавшее в вашем образце, молча исчезает из разобранных данных.
В каждой позиции ниже ваш образец содержал только null, поэтому схема не принимает там ничего другого.
Позиции: 1
Customer.phone
В каждой позиции ниже все числа были целыми. z.number() принимает и 7.5, а .int() потребовал бы целых чисел, поэтому добавляйте его сами только там, где точно знаете, что значение должно быть целым.
Позиции: 1
LineItem.quantity
В каждой позиции ниже ничего не удалось вывести, поэтому схема не проверяет, что там находится: z.unknown() принимает любое значение, а z.record(z.string(), z.unknown()) — любой объект.
Позиции: 1
Root.tags[]
Проверка, которая срабатывает каждый раз, когда приходят данные
Тип TypeScript проверяется, когда ваш код компилируется, и к моменту его выполнения от него уже ничего не остаётся. Схема Zod — та часть, которая остаётся: она работает внутри вашей программы и проверяет каждую полезную нагрузку по мере поступления — ответ API, тело вебхука, сообщение из очереди — до того, как ваш код на неё положится. Вставьте образец этого JSON, и эта страница напишет схемы за вас, готовые к вставке в модуль: каждый объект, у которого есть ключи, становится именованным "z.object", элементы массива объединяются в один, а за каждой схемой следует тип TypeScript, который она порождает.
Это ответ, который страница JSON в TypeScript даёт на тот же JSON, только записанный как валидатор, а не как типы. Обе страницы читают один и тот же вывод типов, поэтому то, какие ключи необязательны, какие значения становятся union, где null держится отдельно от отсутствующего ключа и как называется каждый вложенный объект, решается один раз и лишь записывается дважды. Эти правила объединения — предмет руководства на странице JSON в TypeScript, и здесь они не пересказываются. Это руководство — о том, что схема делает с настоящими данными, когда работает, и что она оставляет решать вам.
Что проходит, а что отвергается
Схема с этой страницы принимает образец, по которому она написана, и в Zod 3, и в Zod 4, за исключением единственного случая, у которого есть собственное замечание: бесконечного числа в Zod 4. За пределами этого образца она принимает всё, что остаётся в рамках сказанного схемой, и для данных, которые вы действительно будете получать, это выглядит так:
- До предела глубины ключ, записанный без ".optional()", обязателен. Полезная нагрузка, в которой его нет, отвергается, как и та, что кладёт в него значение такого вида, которого схема не называет, — строку там, где написано "z.number()", объект там, где написано "z.string()".
- Ключ, записанный с ".optional()", может отсутствовать, а ключ с ".nullable()" может содержать null. Ни то ни другое не пропускает значения никакого другого вида, так что необязательный ключ, если он присутствует, всё равно должен содержать то, что называет схема.
- "z.union" принимает любой из своих членов и ничего больше. "z.array" принимает любое число элементов, в том числе ни одного, если каждый из них подходит под схему, написанную для его элементов.
- Там, где образец ничего не показал, — элементы пустого массива, объект без ключей, значение, вложенное глубже предела глубины, — схема не проверяет, что там находится: она принимает любое значение или любой объект там, где у объекта в образце не было ключей, а замечание сообщает, где именно.
Ключ, которого схема не перечисляет, пропускается, а затем отбрасывается из результата; это и есть отбрасывание, и ему посвящён отдельный раздел. Один ключ стоит в стороне от всего этого. Ключ с именем "__proto__" записывается как вычисляемый ключ, в квадратных скобках, потому что, записанный обычным образом, он задал бы прототип объекта, в котором находится, вместо того чтобы назвать ключ, — и всё же ни одна из версий Zod не возвращает этот ключ в том, что отдаёт разбор, а Zod 4 и вовсе не проверяет его значение.
Почему схема никогда сама не становится строже
У каждого образца есть общие черты, которых он не может доказать: каждый id — целое число, каждый email похож на email, в роли всегда стояло только admin. Это закономерность, а закономерность — не проверка. У генератора есть соблазн всё равно её записать — ".int()" на идентификаторы, "z.email()" на адреса, литерал на роль, — а этот не делает этого никогда, потому что образец показывает, что ваши данные могут содержать, и никогда — что они обязаны содержать. Тип, который строже ваших данных, стоит вам ошибки компиляции на вашей собственной машине. Схема, которая строже ваших данных, стоит вам отвергнутого запроса в продакшене: первой роли, которая не admin, первого id, равного 7.5, первого адреса, которого не предусмотрел шаблон.
К тому же сами проверки менялись бы у вас под ногами. "z.uuid()" в Zod 4 отвергает строки в форме UUID, которые принимал ".uuid()" в Zod 3, потому что проверяет биты варианта, а ".int()" в Zod 4 отвергает целое число за пределами безопасного диапазона, которое пропускал его аналог в Zod 3, — так что проверка, угаданная по сегодняшнему образцу, оказалась бы другой проверкой в зависимости от того, какой Zod вы установите. Поэтому ничего из того, на что образец лишь намекает, в схему не записывается. Там, где это важно при поступлении данных, страница вместо этого сообщает вам об этом в замечании, а правка остаётся за вами.
Чего схема сама о себе не скажет
Под схемой страница перечисляет то, что заметила и не вписала: по замечанию на каждый применимый вид из перечисленных ниже, один раз и со всеми позициями, к которым оно относится. Позиция записывается так, как вещи называет сгенерированный код, — "Customer.phone" для ключа, "Root.tags[]" для элементов массива, ключ в кавычках и квадратных скобках там, где он не является идентификатором, и для "__proto__", — чтобы её можно было найти в схеме с первого взгляда. Пример, который загружается вместе со страницей, показывает все виды, кроме замечания об Infinity.
- Только null. В этой позиции образец никогда не содержал ничего, кроме null, поэтому схема говорит "z.null()" и отвергает первое настоящее значение. Решите, что поле будет содержать, когда оно заполнится, и напишите это сами — например, "z.string().nullable()" — или вставьте образец, в котором у него есть значение.
- Infinity. Число, выходящее за пределы того, что может вместить число в JavaScript, например 1e999, при разборе JSON становится Infinity или -Infinity. "z.number()" в Zod 4 отвергает бесконечное число, а его аналог в Zod 3 принимает, так что в Zod 4 это единственный случай, когда схема отвергает тот самый образец, по которому была сделана. Вопрос, который это поднимает, касается данных, а не схемы: цифры теряются ещё до того, как их увидит какой-либо валидатор, поэтому должно ли это значение вообще быть числом — решать тому, что его записало.
- Целые числа. Все числа в этой позиции были целыми, а "z.number()" принимает и 7.5. Там, где значение должно быть целым, — идентификатор, счётчик, количество — добавьте ".int()" сами; там, где это цена, которая просто оказалась круглой, оставьте как есть. Замечание не выдаётся для позиции, содержащей целое число за пределами безопасного диапазона, от "Number.MIN_SAFE_INTEGER" до "Number.MAX_SAFE_INTEGER", потому что ".int()" в Zod 4 такие числа отвергает, а совет, из-за которого схема отвергла бы собственный образец, — единственный вид совета, которого страница не даст.
- Нечего вывести. Элементы пустого массива, объект без ключей и значение, вложенное глубже 100 уровней, не дают выводу типов ничего, на что можно опереться, поэтому схема не проверяет, что там находится: "z.unknown()" принимает вообще любое значение, а "z.record(z.string(), z.unknown())" — любой объект. Вставьте образец, в котором у этого массива есть элементы, а у этого объекта — ключи, или напишите эту часть схемы вручную.
Замечание никогда не меняет в схеме ни байта. Это предложение рядом с ней, на языке страницы, а вносить правку, на которую оно указывает, или пропустить её — решать вам. Замечаний о форматах строк, литералах или enum нет: каждое было бы догадкой о правиле, которое образец показать не может.
Ключи, которых схема не перечисляет, отбрасываются
"z.object" пропускает объект с ключами, которых он не перечисляет, и оставляет их за пределами того, что возвращает. Так Zod ведёт себя по умолчанию в обеих версиях, и это самое тихое из того, что делает схема: поле, которого случайно не оказалось в вашем образце, исчезает из данных, которые получает ваш код, и никакая ошибка об этом не сообщает. Именно поэтому об этом говорит предложение под каждой схемой на этой странице.
Страница не выбирает за вас между строгим и нестрогим объектом, потому что каждый из них утверждает больше, чем может показать образец. Строгий объект отвергает любой ключ, которого он не перечисляет, — никаких ключей, кроме этих, — и никакой образец не может доказать это о следующей полезной нагрузке. Нестрогий объект сохраняет лишние ключи, и его выведенный тип получает для них индексную сигнатуру, так что он перестал бы быть ответом страницы TypeScript. Отбрасывание — единственное поведение, которое принимает то, что принимает тип TypeScript, и при этом выводит тот же самый тип: значение с лишними свойствами тоже удовлетворяет интерфейсу. Чтобы выбрать иначе, измените схему вручную:
- Чтобы отвергать ключи, которых схема не перечисляет, в Zod 4 напишите "z.strictObject" там, где в сгенерированном коде стоит "z.object", или в Zod 3 допишите ".strict()" к "z.object".
- Чтобы сохранять их, в Zod 4 напишите "z.looseObject", или в Zod 3 допишите ".passthrough()". Zod 4 по-прежнему выполняет оба этих метода Zod 3 и называет их устаревшими (legacy).
Каждый объект в сгенерированном коде — отдельная схема, поэтому выбор делается по одному объекту за раз: если сделать корень строгим, в объектах, вложенных в него, ничего не изменится, и часто это именно то, что нужно, когда настаивать вы вправе только на внешней оболочке.
Одно имя для схемы и её типа
За каждой схемой следует её тип — "export type Customer = z.infer<typeof Customer>" сразу после "export const Customer", — и так zod.dev пишет собственные примеры: одно имя для значения, которое проверяет данные, и для типа, который оно порождает, поскольку TypeScript держит значения и типы в разных пространствах имён. Этот тип — тот же, что страница JSON в TypeScript печатает для того же JSON, имя в имя и ключ в ключ, с теми же необязательными ключами, теми же union и null на тех же местах, — за исключениями, описанными ниже.
Репозиторий проверяет это обещание, а не верит ему на слово: корпус образцов проходит через обе страницы, а затем через компилятор TypeScript с каждой версией Zod, и для каждого имени компилятор спрашивают, идентичны ли два типа и присваивается ли каждый из них другому. Там, где он отвечает иначе, место находится глубоко в документе. За пределом глубины, где ключ содержит "z.unknown()", Zod 3 выводит этот ключ как необязательный, тогда как страница TypeScript делает его обязательным. А в Zod 4 компилятор сдаётся на типе массива, вложенного на десятки уровней, и сообщает об ошибке TS2589 на собственной строке этого типа; схема выше этой строки по-прежнему работает и по-прежнему принимает образец, а теряется только выведенный тип.
Оба сравнения читают необязательный ключ так, как это по умолчанию делает TypeScript. При "exactOptionalPropertyTypes", который выключен, пока проект его не включит, "z.infer" необязательного ключа допускает ещё и явный undefined, в любой версии Zod, тогда как тип страницы TypeScript его не допускает.
Написано для Zod 4 и по-прежнему годится для Zod 3
Сгенерированный код ограничивается тем, что есть в обеих основных версиях, — "z.object", "z.array", "z.union" из двух и более членов, "z.string()", "z.number()", "z.boolean()", "z.null()", "z.unknown()", "z.record(z.string(), z.unknown())", ".optional()", ".nullable()" и "z.infer", — под той строкой импорта, с которой начинаются собственные примеры zod.dev. Ничто в нём не появилось впервые в Zod 4, и ничто не объявлено там устаревшим, так что проект, ещё не ушедший с Zod 3, может вставить его как есть.
Однако один и тот же текст ведёт себя в двух версиях по-разному, и каждое различие названо в этом руководстве там, где оно важно. "z.number()" в Zod 4 отвергает бесконечное число, тогда как его аналог в Zod 3 принимает его, — это и есть замечание об Infinity. Ключ со значением "z.unknown()" необязателен в выведенном типе Zod 3 и обязателен в типе Zod 4 — а начиная с Zod 4.4 обязателен и при разборе данных, — и такой ключ этот код пишет только за пределом глубины. Zod 3 проверяет ключ с именем "__proto__", а Zod 4 — нет. И компилятор сдаётся на типе Zod 4 для массива, вложенного на десятки уровней, тогда как тип Zod 3 он выводит.
Код написан для обычного Zod, с методами: "z.string().nullable().optional()". Zod Mini вместо этого записывает ту же схему функциями, "z.optional(z.nullable(z.string()))", поэтому Zod Mini не может выполнить этот код в нынешнем виде.
Почему корень идёт последним
Страница TypeScript печатает корень первым, а объекты, которые он использует, — после него, поскольку тип можно использовать до строки, в которой он объявлен. Со схемой так нельзя: это значение, а чтение "const" выше его собственного объявления выбрасывает ReferenceError, пока модуль ещё загружается. Поэтому каждая схема здесь идёт после каждой схемы, которую она использует, а корень — последним: в примере сначала Customer и LineItem, затем Root, который их содержит, — вот почему корень, открывающий страницу TypeScript, закрывает эту.
Такой порядок существует всегда. Вывод типов — это дерево, и каждый извлечённый им объект используется ровно в одном месте, так что ни одной схеме не нужно ссылаться на саму себя или на схему, напечатанную после неё, и сгенерированному коду никогда не нужен "z.lazy".
Частые вопросы
- Почему id — это "z.number()", а не "z.number().int()"?
- Потому что образец может показать, что каждый id до сих пор был целым, но не то, что следующий тоже будет целым. Вместо этого страница так и говорит: замечание о целых числах перечисляет каждую позицию, где все числа были целыми, и там, где вы знаете, что значение должно оставаться целым, добавить ".int()" — правка в одно слово. Замечание не выдаётся там, где число — целое за пределами безопасного диапазона, поскольку ".int()" в Zod 4 отверг бы сам этот образец.
- Почему адрес электронной почты выходит простым "z.string()"?
- Потому что строка, похожая на email в вашем образце, ничего не говорит о следующей, а правильная проверка формата должна следовать собственным шаблонам Zod, которые меняются от версии к версии: "z.uuid()" в Zod 4 уже отвергает строки, которые пропускал ".uuid()" в Zod 3. Даты, URL и UUID остаются строками по той же причине, а поле, в котором всегда было лишь несколько значений, никогда не становится enum или литералом. Если вы знаете правило, впишите его сами; схема — это обычный код, который принадлежит вам.
- Почему мой собственный образец не проходит в Zod 4?
- В нём есть число за пределами того, что может вместить число в JavaScript, — скажем, 1e999, — которое при разборе JSON стало Infinity или -Infinity, а "z.number()" в Zod 4 отвергает бесконечное число, тогда как его аналог в Zod 3 принимает. Замечание об Infinity называет каждую затронутую позицию. Если не считать этого случая, схема всегда принимает образец, из которого получена, поскольку в неё вошло каждое значение образца, и репозиторий проверяет это в обеих версиях на корпусе образцов.
- Почему поле, которое я отправил, исчезло из разобранного результата?
- Потому что схема его не перечисляет. "z.object" принимает объект с лишними ключами и возвращает его без них, в любой версии, а ключ, которого не было в вашем образце, — это ключ, которого схема так и не узнала. Добавьте его в схему или сделайте нестрогим только этот объект — "z.looseObject" в Zod 4, ".passthrough()" в Zod 3, — если неизвестные ключи должны проходить нетронутыми.
- Я переместил одну схему ниже другой и получил ReferenceError. Почему?
- Потому что схема — это значение, а JavaScript не позволяет прочитать значение выше строки, в которой оно определено. Именно поэтому страница печатает каждую схему после всех схем, которые она использует, с корнем в конце; держите схему выше всего, что на неё ссылается, и ошибка исчезнет.
- Почему схема называется Customer, а не CustomerSchema?
- Таково соглашение самого zod.dev: схема и выводимый ею тип носят одно имя, что TypeScript допускает, поскольку значения и типы живут в разных пространствах имён. Само же имя — то, которое страница JSON в TypeScript даёт тому же объекту, так что это одно и то же слово на обеих страницах — и для схемы, и для её типа.
- Для какой версии Zod написан этот код?
- Для Zod 4, без всего, чего нет в Zod 3, поэтому он без изменений работает в любой из них. Версии различаются в нескольких местах — бесконечное число, ключ за пределом глубины, ключ с именем "__proto__" и тип очень глубоко вложенного массива, — и каждое из них объяснено выше. Это обычный Zod с цепочками методов; Zod Mini записывает ту же схему функциями и выполнить этот код в нынешнем виде не может.
- Можно ли вставить реальные данные вместе с учётными данными?
- Да. Схема строится в вашем браузере: то, что вы вставляете, читается на вашем собственном устройстве и не отправляется на сервер, не сохраняется и не логируется. К тому же в схеме нет ни одного вашего значения — только ваши ключи и вид значения под каждым из них, так что токен в образце выходит как "z.string()", и не более того.
Похожие инструменты
- JSON в TypeScript
Схема с этой страницы работает как часть вашего кода и проверяет данные каждый раз, когда они приходят. Та страница даёт тот же ответ для того же JSON в виде простых типов TypeScript, под теми же именами: они проверяются при компиляции вашего кода и ничего не добавляют во время выполнения.
- JSON в Go
Эта страница оставляет вам решать, требовать ли целые числа там, где все числа в вашем образце были целыми. Та страница вычисляет ту же форму под теми же именами типов, но в Go нет типа, который был бы просто числом JSON, поэтому там, где каждое число записано как целое, она даёт полю целочисленный тип, и значение, записанное с десятичной точкой, в него не декодируется.
- Тестер JSONPath
Проверка запросов JSONPath (RFC 9535) к JSON.
- Генератор таблиц Markdown
Создание и выравнивание таблиц Markdown из CSV, TSV или JSON.