Ошибка, которая, наконец, заставила меня прекратить рукописный API-типы, была смущающе маленькой. вернулась конечная точка платежей discount: null для клиентов без него, и я напечатал это discount: number потому что единственный ответ, на который я смотрел при написании интерфейса, оказался клиентом со скидкой.TypeScript был совершенно счастлив. компилятор не имел возможности узнать I'd солгал ему. три недели спустя а .toFixed(2) на этом поле было начато производство именно для той группы пользователей, которая имела наименьшее значение для моего тестирования и наибольшее значение для счета-фактуры.
В этом вся проблема с набором API вручную: вы вводите то, что вводите верить конечная точка возвращается, и компилятор послушно обеспечивает соблюдение вашей веры, а не реальности. Каждая гарантия, которую TypeScript дает вам в дальнейшем, хороша только так же, как тот первый рукописный интерфейс, и в цепочке инструментов нет ничего, что проверяло бы ее на соответствие реальному ответу. Вы получаете всю церемонию статической типизации без какой-либо безопасности, что, возможно, хуже, чем отсутствие типов вообще - по крайней мере, нетипированный код вызывает у вас подозрения.
Генерация типов из реальной полезной нагрузки переворачивает направление Вместо описания того, что вы думаете форма, вы берете ответ, который сервер действительно отправил, и получаете форму из него. вывод механический: нет оптимизма, нет полей, которые вы забыли существовали, нет number где данные говорят number | null. Я создаю [Toolz.dev](/и ставлю браузер Конвертер JSON в TypeScript вот это и делается, но это руководство посвящено самим правилам вывода - что может выяснить генератор, о чем он может только догадываться и где еще нужно думать.
TL;DR: Чтобы преобразовать JSON в TypeScript, сделайте вывод о каждом ключе и #39;s типа из его значения (
string,number,boolean,null), извлекают вложенные объекты в их собственные именованные интерфейсы, и объединяют массивы объектов в единый интерфейс элементов, где любой ключ, отсутствующий в некоторых членах, становится необязательным. решайте намеренно, является лиnullсредствоkey?: Tилиkey: T | null- этот выбор зависит от того, пропускает ли ваш API отсутствующие поля или отправляет их как нулевые. Вывод отражает только предоставленную вами выборку, поэтому используйте репрезентативную полезную нагрузку с несколькими записями и рассматривайте выходные данные как рассмотренный первый проект, а не как готовый контракт.
Зачем генерировать типы TypeScript из JSON вместо их написания?
Честный ответ заключается в том, что рукописные типы дрейфуют и генерируются типы don't. Когда серверная часть добавляет поле, ваш рукописный интерфейс молча остается неправильным; ничего не ошибается, потому что дополнительные свойства в ответе невидимы для типа, который не 't упоминать их. Когда серверная часть меняется id от числа до строки ваш интерфейс продолжает настаивать на этом 's число и TypeScript продолжают соглашаться, вплоть до тех пор, пока что-то не объединяется, а не добавляется.
Там's также простой аргумент tedium. типичный ответ REST имеет тридцать ключей на четырех уровнях вложенности. транскрипция, что вручную занимает десять минут чистой механической работы, а механическая работа, выполняемая людьми, имеет частоту дефектов. вы будете вводить имя ключа. вы пропустите одно поле, которое's массив объектов, а не массив строк. генератор не будет.
Но самая сильная причина в том, что поколение формирует форму видимый. Вставьте ответ в конвертер, и вы сразу увидите вещи, которые вы ' бы замалчивали чтение необработанного JSON: вот что metadata на самом деле это глубоко вложенный объект tags иногда пуст, что половина ключей в вашем списке с разбивкой по страницам отсутствует в некоторых записях. сгенерированный интерфейс представляет собой сводку данных 's реальной структуры, и чтение его часто является самым быстрым способом понять конечную точку, которую вы не написали 't написать. I've использовал его в качестве этапа документирования более одного раза на API, чьи документы были ложью.
Если это соответствует другим вашим инструментам обработки данных: если вы & #39; проверяете полезную нагрузку, а не вводите ее, Форматтер JSON это лучшая первая остановка, и если вы & # 39; сравните два ответа, чтобы увидеть, что изменилось между версиями, JSON диф отвечает на это напрямую.
Как на самом деле работает вывод типов из JSON?
JSON имеет шесть типов значений, пер RFC 8259: объект, массив, строка, номер, true/false, и null. TypeScript's примитивные типы почти напрямую отображаются на четырех из них. Интересная работа полностью находится в двух других.
Примитивы тривиальны. Строковое значение подразумевает string. Число подразумевает number- обратите внимание, что у JSON один числовой тип, поэтому в данных нет информации, указывающей вам, есть ли 1 является целым числом или плавающей, и TypeScript в любом случае различает 't. true или false подразумевает boolean. Эта часть не имеет двусмысленности.
Объекты становятся интерфейсами. Каждое значение объекта становится именованным интерфейсом, а ключ, под которым он появился, предоставляет имя, преобразованное в PascalCase. Ключ owner производит interface Owner. Вложение повторяется: объект внутри объекта создает второй интерфейс, на который ссылаются из первого. Это имеет большее значение, чем звучит. Альтернатива - анонимное встраивание каждой вложенной формы - создает одно нечитаемое объявление и не дает вам ничего важного:
// Inlined: technically correct, practically useless
interface Project {
owner: { id: number; email: string; twoFactor: boolean }
}
// Extracted: you can import and reference Owner on its own
interface Project {
owner: Owner
}
interface Owner {
id: number
email: string
twoFactor: boolean
}
Один раз Owner существует как имя, функция, которая принимает только владельца, может быть набрана (owner: Owner) => void. С встроенной версией вы 'd пишите Project['owner'] везде, что работает, но плохо читает.
Массивы - это то место, где живут реальные решения. Тип массива's - это объединение типов его элементов, так что [1, 2, 3] дает number[] а [1, "a"] дает (number | string)[]. Обратите внимание на скобки во второй - без них, number | string[] означает нечто совершенно иное (число или массив строк) и генераторы, которые забывают об этом, излучают код, который компилирует, но описывает неправильную вещь.
Пустые массивы - это честный тупик. "tags": [] говорит вам ключ существует и держит массив; он ничего не говорит вам о том, что в нем идет Правильный вывод unknown[], и вы должны прочитать, что как генератор отказывается от угадывания, а не как законченный ответ. Заполните его в себе из документации, или найдите образец, где массив 't пуст.
Почему массивы объектов объединяются, а не объединяются?
Это единственное решение, которое отделяет генератор you'd использовать от одного you'd отказаться через пять минут.
Рассмотрим страничный ответ, в котором записи 't совершенно однородны, то есть каждый реальный страничный ответ:
{
"rows": [
{ "id": 1, "name": "Ada", "nickname": "The Countess" },
{ "id": 2, "name": "Grace" }
]
}
Обрабатывайте каждый элемент независимо, и вы получаете объединение двух интерфейсов: rows: (Row1 | Row2)[]. Это технически самое точное считывание образца, и оно бесполезно Каждый доступ к row.nickname теперь требует сужения, потому что TypeScript может't знать, какой член союза у вас есть. Распространите это на ответ пятьдесят записей с несколькими дополнительными полями, и вы получите объединение десятков почти идентичных интерфейсов. Никто этого не хочет.
Полезным показанием является то, что эти два объекта являются двумя экземплярами одного объекта nickname это поле, которое есть у Грейс и #39;t имеет:
interface Row {
id: number
name: string
nickname?: string
}
interface T {
rows: Row[]
}
Это 's слияние: собрать каждый ключ, видимый во всех элементах, и отметить ключ необязательным, если он 's отсутствует в любом из них. Он соответствует тому, как данные на самом деле производятся - одна таблица базы данных, один сериализатор, некоторые столбцы, подлежащие нулю, - и он создает типы, которые вы можете использовать без церемоний. Имя элемента массива также сингулярно, поэтому releases урожайность Release а не Releases, потому что releases: Releases[] читается как ошибка, даже если это 't.
Компромисс реален и стоит заявить прямо: слияние предполагает, что массив однороден. Если у вас действительно гетерогенный массив - лента событий разной формы, различаемая a type поле - объединение сглаживает отдельные варианты в один интерфейс, где почти все не является обязательным. That's неверная модель, и it's случай, когда вы должны взять сгенерированный вывод в качестве отправной точки и написать от руки правильное различаемое объединение. Генераторы don't знают ваш домен.Этот объединяет объекты и объединяет все остальное, что в большинстве случаев правильно и неправильно, чтобы вы могли сразу заметить.
Должен ли null стать необязательным ключом или членом профсоюза?
Оба соглашения оправданы, и разница кусается, поэтому принимайте решения намеренно, а не принимайте все, что по умолчанию используется вашим инструментом.
Дано { "retiredAt": null }, есть два чтения:
interface A { retiredAt?: string } // the field may be absent
interface B { retiredAt: string | null } // the field is present and may be null
Они не взаимозаменяемы. в A, retiredAt есть string | undefined а ключ может вообще не существовать на объекте В B, ключ всегда существует, и его значение может быть null. Под. strictNullChecks- который Справочник TypeScript рекомендует и что вам следует делать - оба заставляют вас вести отсутствующий случай, но они заставляют проводить разные проверки и сериализуются по-разному. JSON.stringify опускает undefined свойства полностью и излучает null для нулевых выбор распространяется до самого конца.
Правильный ответ зависит от фактического поведения API's, которое ни один генератор не может увидеть по одному образцу:
| Ваше поведение API и #39;s | Правильная модель | почему |
|---|---|---|
| Опускает ключ, когда нет значения 's | key?: T |
Ключ действительно есть 't там; опционально точен |
Всегда отправляет ключ, null когда пустой |
key: T | null |
Ключ всегда присутствует; ? ошибочно допускал бы отсутствие |
| Несогласованные - иногда опущенные, иногда нулевые | key?: T | null |
Оба случая реальны; моделировать оба |
Отправляет null только на ответах об ошибках |
Ни - моделировать ошибку отдельно | Недействительное поле скрывает объединение форм ответа |
Эта последняя строка - та, на которой стоит остановиться Поле, которое становится нулевым только в случаях сбоя, является сигналом о том, что конечная точка возвращает две разные вещи, носящие одну форму, а исправление - это дискриминируемое объединение в поле состояния, а не свойство, которое можно аннулировать. Генерация типа покрывает этот шаблон; это не 't решает его.
Преобразователь по умолчанию key?: T потому что ospose-when-absent - более распространенное соглашение в JSON API I've работал с, и потому что он лучше компонуется с описанным выше слиянием массивов (ключ отсутствует в некоторых записях и ключ, который's null в некоторых записях моделируется таким же образом).Выключите опцию и null вместо этого остается в союзе. ни один из них не является трюком; выберите тот, который на самом деле делает ваш API.
А как насчет ключей, которые являются 't действительными идентификаторами TypeScript?
Объектные ключи JSON - это произвольные строки. имена свойств TypeScript в пустом виде key: T позиция не - они должны быть действительными идентификаторами. Итак "content-type", "2fa", "user.name", и "" все это законные ключи JSON, которые нельзя записать без кавычек в интерфейсе.
Исправление - цитирование, и it's не обходной путь - цитируемые названия свойств являются обычными TypeScript:
interface Headers {
"content-type": string
"2fa": boolean
class: string
}
Доступ к этим свойствам осуществляется с помощью скобочных обозначений (headers["content-type"]), который немного более многословный, но полностью безопасный для шрифтов. Обратите внимание, что class does't нужно цитировать: зарезервированные слова совершенно законны названия свойств, даже несмотря на то, что они & #39; являются незаконными в качестве идентификаторов Ограничение применяется только в том случае, если TypeScript ожидает идентификатор - именно поэтому то же слово действительно нуждается в обработке, когда оно становится интерфейсом имя, йо-
Имена интерфейсов, полученные из таких ключей, требуют больше работы, чем цитирования. 2fa ПаскальДела к 2fa, который может't запустить идентификатор, так что он получает префикс. два разных вложенных объекта оба под ключами названы owner хотели бы оба быть Owner, так становится второй Owner2. Это негламурные детали, и они ' это именно те детали, которые решают, компилируется ли сгенерированный выходной сигнал или требуется пятнадцать минут ручного ремонта, прежде чем он это сделает. Тест, к которому я отношу конвертер, прост: вставьте все действительное, и выходные данные должны компилироваться под ним strict без правок.
Интерфейсы или псевдонимы типов?
Генератор излучает либо. практическое различие узкое, но реальное, и ваша кодовая база, вероятно, уже имеет мнение, закодированное в конфигурации lint.
interface User {} поддерживает объединение объявлений - объявляйте одно и то же имя интерфейса дважды и TypeScript объединяет их. That's существенно для дополнения типов из библиотек, которые вы делаете 't управления, и footgun везде, где бы то ни было, так как два несвязанных объявления с одним и тем же именем молча сливаются вместо ошибок Интерфейсы также поддерживают extends, который выдает немного лучшие сообщения об ошибках, чем типы пересечений, когда ограничение не работает.
type User = {} can't сливаются, что обычно является функцией, и it's требуется для всего, что является 't формой объекта: объединения, кортежи, отображенные типы, условные типы. корень, который является 't объектом JSON - массивом чисел, голой строкой - может быть выражен только в виде псевдонима, так что type Nums = number[] это то, что вы получаете независимо от настройки.
Для генерируемых типов API я склоняюсь к interface, главным образом потому, что сообщения об ошибках немного лучше и потому, что риск слияния теоретический, когда каждое имя живет в одном сгенерированном файле Но это близко к подбрасыванию монеты, и согласованность с окружающим кодом имеет большее значение, чем достоинства Если ваша конфигурация ESLint имеет @typescript-eslint/consistent-type-definitions установите в любом случае, сопоставьте это и перестаньте думать об этом.
Чем это отличается от схемы JSON к TypeScript?
Они решают действительно разные проблемы, и it's стоит быть точными, потому что & quot;JSON to TypeScript" и & quot;JSON Schema to TypeScript" находятся на расстоянии одного слова друг от друга и часто путаются.
JSON в TypeScript - это вывод из примера. Ввод: значение. Генератор наблюдает за тем, что 's там и обобщает. Он не может знать, требуется ли поле, ограничена ли строка в перечислении, имеет ли число минимум или репрезентативна ли одна вставленная вами выборка. Это и #39;s индукция из одного наблюдения со всем, что подразумевает.
Схема JSON для TypeScript - это перевод из декларации. Вход: а Схема JSON документ, в котором уже указаны типы, required массивы, перечисления, форматы и ограничения. Генератор 't угадывает - it's транслитерирует существующий контракт в синтаксис TypeScript. required карты необязательных свойств; ан enum сопоставления со строковым буквальным объединением; oneOf карты в тип союза.
Правило следует непосредственно если схема существует, используйте ее. Схема JSON, спецификация OpenAPI, a .proto файл, или схема GraphQL является авторитетным таким образом, что выборочный ответ никогда не является. Вывод - это то, к чему вы обращаетесь, когда схема не существует - недокументированная внутренняя конечная точка, сторонний API, документы которого устарели, формат файла конфигурации, который органично вырос, приспособление you're writing tests against. Which, справедливости ради, описывает большую часть JSON, с которым на самом деле имеет дело любой из нас.
Там's средний путь, о котором стоит упомянуть: используйте вывод бутстрап, затем поддерживать вручную. сгенерировать интерфейс из реального ответа, чтобы получить форму и имена полей правильно, затем отредактировать его - затянуть a string к буквальному союзу, в котором вы знаете допустимые значения, зафиксируйте unknown[] образец остался пустым, разделил объединенный интерфейс на правильное дискриминируемое объединение. Генератор выполняет механические 90%, и вы применяете знания предметной области, которые он структурно не может иметь.
Где вывод делает его неправильным?
Короткий, честный список. каждый из них является ограничением подхода, а не ошибкой в конкретном инструменте, и знание их - это разница между хорошим использованием сгенерированных типов и их сожжением.
Отдельные образцы недоопределяют тип. Поле, которое's number в вашем образце может быть null в 5% записей. поле, которое 's присутствует во всех трех записях, которые вы вставили, может быть необязательным для всего набора данных. вывод сообщает о том, что он видел. Вставьте больше записей - в идеале реальную страницу результатов, а не один выбранный вручную объект - и опциональные варианты станут значительно более точными.
Струны скрывают свои настоящие типы. Временные метки ISO, UUID, URL-адреса и адреса электронной почты - все это просто string в анализатор JSON. "2026-07-16T09:00:00Z" семантически это дата; ничто в данных не говорит об этом. Если ваша кодовая база имеет фирменный знак ISODateString введите, вы'подставляем вручную.
Числа теряют различия в точности. Тип одного номера JSON's означает идентификатор, который's 64-битное целое число на сервере поступает как номер JavaScript и, возможно, уже потеряло точность до того, как ваш генератор когда-либо его увидит - Number.MAX_SAFE_INTEGER это около 9×10¹5, и Twitter, как известно, узнал это на собственном горьком опыте. если ваш API отправляет большие целые числа в виде строк, то 's почему, и сгенерированный string является правильным.
Буквальные значения выглядят как их общие типы. "status": "active" делает вывод string, не "active" | "archived" | "pending". Более узкий тип более полезен, и ни один образец не может его доказать. Это наиболее распространенное ручное редактирование, которое я делаю для сгенерированного вывода.
Пустые контейнеры ничего не говорят. [] дает unknown[] а {} дает пустой интерфейс. оба генератор честный.
Ничто из этого не делает вывод небезопасным - это делает его проект. Рабочий процесс, который работает: генерировать, внимательно читать выходные данные, исправлять четыре или пять вещей, которые вы знаете, что образец мог 't сказать, зафиксировать. Это 's все еще на порядок быстрее и точнее, чем транскрипция тридцати ключей вручную, что является реальной альтернативой.
Мой JSON загружается куда угодно?
Нет, и это категория инструментов, где вопрос заслуживает реального ответа, а не значка.
Подумайте о том, что 's в JSON вы'd вставить в генератор типов. It's ответ API, что означает, что он правдоподобно содержит токен носителя, идентификатор сеанса, электронное письмо клиента, внутренний идентификатор пользователя, уровень цен, секрет веб-крючка. Это's не гипотетический - it's модальный случай, потому что все дело в том, что вы схватили a реальный ответ на тип против.
Любой серверный конвертер обязательно получает эту полезную нагрузку. Он может не регистрировать ее, и, вероятно, это не так 't, но вы 're расширение доверия вы don't должны расширять, и в зависимости от данных вы можете создать проблему соответствия для задачи, которая не имеет никакого бизнеса касаться сети вообще.
Вывод типа - это чистые вычисления по проанализированному значению. Ему не нужна ни сеть, ни учетная запись, ни хранилище. конвертер на Toolz.dev - это несколько сотен строк TypeScript без зависимостей, работающих на вашей вкладке; полезная нагрузка - это строка JavaScript в вашем браузере и #39;s память, и она остается там. Вы можете проверить это так, как вы 'd проверить любое такое утверждение - откройте вкладку сети и нажмите "Создать" или отключите свой Wi-Fi и посмотрите, как он продолжает работать. Это тот же принцип, который лежит в основе каждого инструмента на сайте, и I'вы написали о том, почему это имеет более широкое значение в почему браузерные инструменты превосходят серверные для конфиденциальных данных, йо-
Работающий пример
Здесь's образец, с которым поставляется инструмент, который намеренно создан для выполнения всех приведенных выше правил:
{
"id": 4821,
"name": "Toolz",
"isPublic": true,
"retiredAt": null,
"owner": {
"id": 12,
"email": "[email protected]",
"twoFactor": false
},
"tags": ["developer", "privacy", "browser"],
"releases": [
{ "version": "1.0.0", "downloads": 1420, "notes": "First cut" },
{ "version": "1.1.0", "downloads": 3310 }
]
}
С корнем названным Projectэто порождает:
export interface Project {
id: number
name: string
isPublic: boolean
retiredAt?: null
owner: Owner
tags: string[]
releases: Release[]
}
export interface Owner {
id: number
email: string
twoFactor: boolean
}
export interface Release {
version: string
downloads: number
notes?: string
}
Прочтите, что произошло. owner был извлечен в собственный интерфейс и указан по имени. tags рухнул до string[] потому что каждый элемент был строкой. releases объединила двух своих членов в одно Release- сингуляризированный - и notes стал необязательным, потому что во втором выпуске был 't. retiredAt стал необязательным, поскольку его единственное наблюдаемое значение было нулевым.
А теперь прочитайте, что вы 'd исправить. retiredAt?: null является ли генератор's честным отчетом, что он никогда не видел ненулевого значения, и it's бесполезен как тип - you'd изменить его на retiredAt?: string потому что вы знаете это 's метка времени, когда присутствует. Это единственное редактирование - это весь урок: генератор получил структуру из семи ключей, вложение, слияние массивов и опциональность прямо в одной вставке и оставил вам одно решение, которое требовало знания того, что означает поле.
часто задаваемые вопросы
Как преобразовать JSON в интерфейс TypeScript?
Вставьте свой JSON в конвертер, установите имя корневого типа в любое имя ресурса и нажмите "Генерировать". Он выводит тип каждого ключа, вытягивает вложенные объекты в их собственные именованные интерфейсы, объединяет массивы объектов в один тип элемента и выводит код, который вы можете скопировать прямо в a .ts файл. There's нет регистрации и загрузки - вывод выполняется в вашем браузере.
Что происходит с массивами объектов?
Они're слиты в один интерфейс, описывающий один элемент, и свойство набрано как массив из него Любой ключ, который появляется в одних членах массива, но не в других, становится необязательным Это соответствует тому, как ведут себя реальные данные с разбивкой по страницам, где записи приходят из одной таблицы, а некоторые столбцы аннулируются. Единственный случай, который он плохо обрабатывает, - это действительно гетерогенный массив различных типов событий, который вы должны вручную преобразовать в дискриминируемое объединение.
Должно ли null стать необязательным ключом или объединением с null?
Это зависит от того, опускает ли ваш API отсутствующие поля или отправляет их как нулевые. Если он их опускает, key?: T является точным. если ключ всегда присутствует, а иногда и нулевой, key: T | null является точным и использующим ? ошибочно допустит отсутствие ключа. Преобразователь по умолчанию становится необязательным и позволяет переключаться, потому что они сериализуются по-разному - JSON.stringify отбрасывает неопределенные свойства, но излучает нулевые.
Может ли он вывести точные типы из одного образца JSON?
Он делает вывод о точных типах для этого образца, который является 't то же самое. поле, которое 's число в вашей одной записи может быть нулевым в других; поле, присутствующее во всех трех записях, которые вы вставили, может быть необязательным для всего набора данных. Используйте репрезентативную полезную нагрузку с несколькими записями, а не с одним выбранным вручную объектом, и рассматривайте выходные данные как проверенный черновик, а не как готовый контракт.
Чем 's отличается JSON от TypeScript и JSON Schema от TypeScript?
Этот инструмент выводит типы из примерного значения; JSON Schema в TypeScript переводит формальную схему, которая уже объявляет типы, требуемые поля и перечисления. схема является авторитетной, и вывод является предположением, поэтому, если у вас есть схема JSON, спецификация OpenAPI или схема GraphQL, используйте ее. Вывод предназначен для очень распространенного случая, когда схема не существует и все, что у вас есть, - это тело ответа.
Как он обрабатывает ключи, которые являются 't действительными идентификаторами?
В выходе указываются клавиши с тире, точками, пробелами или ведущими цифрами, так что "content-type" становиться "content-type": string. That's действительный TypeScript, доступ к которому осуществляется с помощью скобок. Зарезервированные слова типа class don't нужно цитировать в качестве имен свойств. Имена интерфейсов, полученные из таких ключей, имеют PascalCased и префикс, если они'd начинаются с цифры, а сталкивающиеся имена получают числовой суффикс, поэтому выходные данные всегда компилируются.
Должен ли я генерировать интерфейсы или вводить псевдонимы?
Соответствуйте тому, что ваша кодовая база уже делает - это в основном вопрос согласованности. Интерфейсы поддерживают объединение деклараций и extendsи дайте немного более четкие сообщения об ошибках. Введите псевдонимы can't merge, что обычно желательно и требуется для всего, что имеет форму объекта 't. Корень, для которого 's массив или примитив излучается в качестве псевдонима в любом случае, поскольку там 's нет объекта, для которого можно объявить интерфейс.
Мой json загружен на сервер?
Нет. весь механизм вывода работает как JavaScript в вашем браузере, без сетевых вызовов, без регистрации и без хранилища. Здесь это имеет большее значение, чем для большинства инструментов, потому что JSON you'd вставить в генератор типов обычно является реальным ответом API, содержащим токены, записи клиентов или внутренние идентификаторы. Откройте вкладку сети во время генерации или отключения от Интернета - она продолжает работать.
Связанные инструменты: Форматтер JSON Для проверки полезной нагрузки в первую очередь json в yaml а json в xml для преобразования формата и JSON диф за то, что заметил, что изменилось между двумя ответами. Дополнительное чтение: Полное руководство по инструментам JSON а Руководство по инструментам кодирования разработчика, йо-



