JSON в Go
Генерирует структуры Go с тегами json из JSON: тип числа — по тому, как оно записано, указатели — там, где значения может не быть. Всё в браузере.
type Root struct {
Orders []Order `json:"orders"`
HasMore bool `json:"has_more"`
}
type Order struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
CreatedAt string `json:"created_at"`
Price float64 `json:"price"`
Coupon *string `json:"coupon"`
GiftNote *string `json:"gift_note,omitzero"`
Shipping Shipping `json:"shipping"`
Tags []any `json:"tags"`
}
type Shipping struct {
City string `json:"city"`
Postcode json.RawMessage `json:"postcode"`
}
В каждой позиции ниже все числа прочитаны как целые, поэтому для неё выбран тип int64. Значение, записанное с десятичной точкой или экспонентой, даже 10.0, в неё не декодируется.
Позиции: 2
Order.IDOrder.UserID
В каждой позиции ниже значения бывают разных типов — например, числа и строки, — поэтому для неё выбран тип json.RawMessage, который хранит каждое значение точно так, как оно записано в вашем JSON, чтобы ваша программа декодировала его, когда узнает, какого оно типа. Если один из этих типов — объект, его структура всё равно есть в выводе, и такое значение можно декодировать в неё.
Позиции: 1
Shipping.Postcode
В каждой позиции ниже не из чего было вывести тип: там встречался только null, массив всегда был пуст, у объекта не было ключей или значение вложено слишком глубоко, чтобы этот инструмент его разобрал. Поэтому для неё выбран тип any, который принимает любое значение, или map[string]any там, где у объекта не было ключей, — он принимает любой объект или null и ничего другого.
Позиции: 1
Order.Tags[]
Структуры, которые декодируют JSON, из которого получены
Вставьте образец JSON, и эта страница напишет для него объявления Go: именованную структуру для каждого объекта в образце и тег json на каждом поле, называющий ключ, который это поле читает. Всё дальнейшее определяет одно обещание. Помещённые в программу, собранную Go 1.27, объявления декодируют вставленный вами JSON через "encoding/json" без ошибки, и каждый ключ читается в поле, кроме ключа, который не может содержать ни один тег структуры: такой ключ оставляется за пределами структуры. Там, где Go не даёт этому обещанию выполниться или заставляет страницу выбрать то, чего не решили ваши данные, замечание под выводом говорит, где именно, — за одним редким исключением, описанным в разделе о тегах.
Сама форма вычисляется ещё до того, как написано хоть что-то на Go, тем же прочтением вашего JSON, которое питает страницу JSON в TypeScript. Объединение элементов массива, выявление ключей, которые есть лишь у части из них, отделение null от ключа, который вообще не присылали, и именование каждого объекта, найденного внутри другого, — всё это описано в руководстве той страницы, поэтому ниже ничего из этого не повторяется. Дальше — то, что касается Go: тип для каждого числа, указатели, тип для ключа со значениями разных типов, имена полей, теги и вопросы, которые поднимают замечания.
Три числовых типа, выбранных по тому, как записано каждое число
Браузер, читающий JSON, превращает 10 и 10.0 в одно и то же число. Go так не делает: поле "int64" не принимает число, записанное с десятичной точкой или экспонентой, — 10.0, 1e3, даже -0.0, — а 11 и -0 принимает. Поэтому страница не выбирает числовой тип по одним лишь значениям. Она спрашивает у собственного парсера браузера, как было записано каждое число, и решает, исходя из этого:
- Там, где каждое число в позиции записано как целое, без десятичной точки и без экспоненты, поле получает тип "int64", и его называет замечание о целых числах.
- Любое другое число получает тип "float64", так что цена, записанная как 10.0, — это "float64", даже если все цены в образце круглые. Модуль "json" в Python записывает float с целым значением именно так, поэтому API на Python — обычное место, где это встречается.
- "json.Number" оставлен для чисел, которые ни один из двух типов не хранит точно: для целого числа за любым из концов диапазона "int64", для числа, слишком большого для "float64" вообще, например 1e999, и для слишком длинного для "float64" целого числа рядом с дробью, как в [9007199254740993, 1.5]. Он хранит каждое из них точно, а ваша программа преобразует его там, где использует значение.
Длинному целому числу среди других целых такая осторожность не нужна: [9007199254740993, 1] — это "[]int64". Дробь, в которой больше цифр, чем хранит "float64", остаётся "float64" и получает собственное замечание, поскольку после декодирования и повторного кодирования 0.30000000000000001 возвращается как 0.3.
Чтобы прочитать, как записано число, нужен браузер, который об этом сообщает. Браузер, который не сообщает, читает каждое число только по его значению, поэтому 10.0 выглядит там целым, и его поле получает тип "int64", а такое поле этот JSON не декодирует; страница подсчитывает числа, которые не смогла отличить от целых, и показывает, где находится первое из них. Такой браузер не видит и цифр, которые отбрасывает "float64", поэтому там длинное целое рядом с дробью получает тип "float64", а дробь, которая округляется, не называется.
Указатели там, где значения может не быть
Ключ, который часть объектов опускает или задаёт равным null, становится указателем: "*string", "*int64" или указателем на структуру, написанную для объекта. Элементы массива, содержащего null, — тоже указатели, так что [1, null] — это "[]*int64". Ключ, отсутствующий в части объектов, вдобавок помечается в своём теге как "omitzero". Когда есть и то и другое, поле может сказать, пришло ли значение, а повторное кодирование декодированной структуры снова пропускает ключ, который в образце всегда только отсутствовал, и снова записывает как null ключ, который в образце всегда был только null.
Срез (slice), отображение (map), "any" и "json.RawMessage" указателя не получают, поскольку каждый из них и так равен nil, когда в него ничего не декодировано. Срез всё же получает "omitzero" там, где его ключ может отсутствовать, а массив, который присутствовал, но был пуст, записывается обратно как [].
Одно различие не сохраняется. Ключ, отсутствующий в одних объектах и равный null в других, в Go — единственный nil, поэтому ваша программа не может различить эти два случая, и повторное кодирование структуры пропускает ключ даже там, где в образце был null; страница называет каждое поле, с которым это происходит. Ключ со значениями разных типов различие сохраняет, потому что "json.RawMessage" хранит null как байты null.
json.RawMessage для смешанных типов и any там, где ничего не встретилось
Когда значения ключа расходятся по типу — число в одном месте, строка или объект в другом, — поле получает тип "json.RawMessage": собственные байты значения, оставленные вашей программе, чтобы она декодировала их, посмотрев, какого типа значение пришло. Значение null среди них ничего не меняет. Если один из типов — объект, структура, извлечённая для него, всё равно печатается рядом с остальными. "any" тоже принял бы любой тип, и для смеси он не используется, потому что число, декодированное в "any", становится "float64" и теряет всё, чего "float64" не может вместить.
В некоторых частях образца нет значения, из которого можно что-то узнать: это ключ, в котором всегда был только null, массив, который каждый раз был пуст, объект без ключей и значение, вложенное глубже, чем читает вывод типов. В Go ключ с null и глубоко вложенное значение получают тип "any", элементы пустого массива — "[]any", а объект — "map[string]any". "any" принимает значение любого типа. Отображение принимает объект или null, а строка, число, массив или логическое значение на его месте приводят к ошибке декодирования.
За этой глубиной замечания умалчивают о двух вещах. Ключ там, отсутствующий в одних объектах и равный null в других, назван лишь как позиция, в которой не из чего вывести тип, и Go, записывая его обратно, пропускает его там, где он был null. А целое число длиннее, чем вмещает "float64", возвращается округлённым, 9007199254740993 — как 9007199254740992, потому что в этой позиции стоит "any".
Имена полей в стиле Go, имена структур — общие со страницей TypeScript
Имя поля — это то, как Go записывает его ключ. Ключ разбивается на слова на каждом символе, который не является буквой или цифрой, и там, где за строчной буквой следует заглавная; затем слова соединяются, каждое с заглавной буквы, так что "user_name", "last-name" и "firstName" становятся UserName, LastName и FirstName. Слово из списка аббревиатур staticcheck по умолчанию пишется заглавными буквами — "id" становится ID, "api_key" — APIKey, "video_url" — VideoURL, — но в счёт идёт только целое слово, так что "idle" — это Idle, а множественное число "ids" — Ids. Ключ, целиком записанный заглавными буквами, читается как слова: "USER_ID" — это UserID.
Собственные буквы ключа сохраняются, так что "имя" становится Имя. Там, где имя не начиналось бы с заглавной буквы, — у ключа в письменности без заглавных букв, например "名前", у ключа, начинающегося с цифры, например "1st", или у первой буквы без заглавной формы из одной буквы, например "ß", — оно получает X спереди: X名前, X1st и Xß, и поле всё равно читает свой ключ. Комбинирующие диакритические знаки в имя не попадают и сохраняются в теге. Ключ, в котором нет ни буквы, ни цифры, например "@", называется Field, а имя, уже занятое в той же структуре, получает число: "user_id", "userId" и "USER_ID" вместе дают UserID, UserID2 и UserID3.
Стиль Go доходит до имён полей и на этом останавливается: каждая структура сохраняет имя, которое вывод типов выбрал для её объекта, так что объявления здесь и интерфейсы на странице TypeScript называются одинаково, а поле может быть названо иначе, чем структура, которую оно содержит. Как выбирается это имя, рассказывает руководство страницы TypeScript.
Теги для Go 1.27 и ключи, которые не может содержать ни один тег
Тег каждого поля называет его ключ в точности, как в json:"user_id", с ,omitzero после ключа там, где ключ может отсутствовать. Управляющий символ в ключе записывается той escape-последовательностью, которую использует сам Go, а ключ "-" записывается как json:"-,", в форме, которую Go читает именно как этот ключ.
Теги пишутся для Go 1.27. Go 1.26 читает через тег меньше ключей: ключ, содержащий что-либо, кроме букв, цифр, пробела ASCII и набора знаков пунктуации ASCII, — "Price (€)", "temp °C", эмодзи, управляющий символ, комбинирующий диакритический знак, — там не читается, и страница называет каждое такое поле. Эта половина проверяется на Go 1.27, собранном с "GOEXPERIMENT=nojsonv2", который читает теги так же, как Go 1.26, но с более новыми таблицами Unicode из Go 1.27, поэтому ключ с буквой, которой нет в более старых таблицах Go 1.26, в этой проверке читается и замечания не вызывает.
Ключ, содержащий запятую, обратный слэш, двойную кавычку, апостроф или обратный апостроф, а также пустой ключ — это ключ, который не может содержать ни один тег структуры, поэтому поля он не получает вовсе. Ваш JSON всё равно декодируется: "json.Unmarshal" без возражений пропускает такой ключ, хотя "json.Decoder", настроенный на "DisallowUnknownFields", на нём останавливается. Замечание перечисляет такой ключ по его структуре и по самому ключу в кавычках, как Go записывает строку, например Order["note,internal"], а для объекта под таким ключом структура всё равно печатается. Один случай проходит без замечания: два ключа, различающиеся лишь одиночным суррогатом — escape-последовательностью, которая не обозначает никакого символа, — для Go один ключ, и ни одно из их полей не заполняется.
Только объявления, в оформлении gofmt
Вывод содержит объявления типов и ничего больше: ни строки package, ни import, поэтому он вставляется в уже имеющийся у вас файл, под собственной строкой package этого файла. Там, где используются "json.RawMessage" или "json.Number", файл также импортирует "encoding/json", и это единственная строка, оставленная вам или вашему редактору. Оформление — то самое, что у gofmt: табуляция перед каждым полем и имена, типы и теги каждой структуры, выровненные по столбцам, — поэтому gofmt оставляет его ровно таким, как есть. Тип корня печатается первым, каждый объект — именованный тип, а не структура, записанная прямо внутри поля, которое её содержит, а JSON, который на верхнем уровне является массивом или одиночным значением, — тоже именованный тип, например "type Root []RootItem", так что всегда есть тип, в который его можно декодировать.
Что замечания просят вас проверить
Каждый вид решения, навязанного Go, получает одно замечание под объявлениями, и это замечание собирает все позиции, к которым оно относится, так что образец со множеством целочисленных полей получает одно замечание, а не по одному на поле. Позиции записываются так, как их записывают объявления (Order.UserID — поле, Order.Tags[] — элементы массива, а корень — просто его имя), кроме двух замечаний, которые вместо этого указывают внутрь вашего JSON строкой и столбцом: ключ, записанный дважды, и числа, которые этот браузер не смог отличить от целых. Ни одно из них не меняет объявлений. Читайте каждое как вопрос о ваших данных:
- Целые числа с типом "int64". Все числа там были записаны как целые. Если значение, которое придёт позже, может нести десятичную точку или экспоненту, как у цены или измерения, оно не декодируется, поэтому сделайте это поле "float64"; идентификатор или счётчик могут остаться как есть.
- "json.Number". Ни один другой числовой тип не хранит эти значения точно. Преобразуйте каждое там, где ваша программа его использует, или, если вы знаете, что настоящие значения помещаются в более узкий тип, измените поле сами.
- Дробь, которую округляет "float64". В числе там больше цифр, чем хранит "float64", поэтому ваша программа видит близкое значение, а не записанное. Там, где важна каждая цифра, сделайте поле "json.Number".
- Значения разных типов, сохранённые как "json.RawMessage". Смотрите на каждое значение, когда оно приходит, и декодируйте его как тот тип, которым оно оказалось; если один из типов — объект, его структура всё равно есть в выводе, чтобы декодировать такое значение в неё.
- Не из чего вывести тип. Поле имеет тип "any" или это отображение со значениями "any", потому что в образце там не было значения, из которого можно что-то узнать. Замените его типом, который, как вы знаете, несёт это поле, или преобразуйте заново из JSON, где у него есть настоящие значения.
- Отсутствует в одних объектах, null в других. Go хранит для обоих случаев один nil, поэтому при обратной записи ключ пропускается. Это важно, только если то, что читает ваш вывод, обрабатывает null иначе, чем ключ, которого нет.
- Ключи, которые Go 1.26 не читает. Собирайте с Go 1.27 или переименуйте эти ключи там, где они создаются; в Go 1.26 они остаются непрочитанными.
- Ключи, оставленные за пределами структуры. У них нет поля. Чтобы прочитать такой ключ, декодируйте объект в "map[string]json.RawMessage" или напишите для структуры метод "UnmarshalJSON", как советует само замечание.
- Ключ, записанный дважды. Типы строятся по последней копии ключа, но Go декодирует каждую копию по очереди, поэтому более ранняя копия, которую тип не может вместить, например строка там, где последняя копия — число, заставляет декодирование вернуть ошибку, а ключ, который есть только в более ранней копии объекта, не читается. Исправьте JSON в показанных строке и столбце.
- Этот браузер не отличает 10 от 10.0. Он не сообщает, как записано число, поэтому поле, получившее тип "int64" из-за значения, записанного с десятичной точкой или экспонентой, не декодирует ваш JSON. Проверьте поля, записанные таким образом, или преобразуйте JSON в браузере, который об этом сообщает.
Замечаний о датах, форматах или фиксированных наборах значений нет, потому что страница никогда не угадывает их по строке: "created_at" в примере остаётся "string", как бы оно ни выглядело.
Частые вопросы
- Почему моя цена — float64, если все цены в моём JSON круглые?
- Потому что каждая цена записана с десятичной точкой, как 10.0, а поле "int64" не принимает число, записанное так, будь оно круглым или нет. Страница читает, как записано каждое число, а не только его значение, поэтому поле получает тип "float64", и ваш JSON декодируется. С типом "int64", выбранным по одним значениям, декодирование остановилось бы на первой же цене.
- Когда число получает тип json.Number?
- Когда одно из его значений — целое число за пределами диапазона "int64", число, слишком большое для "float64", или длинное целое рядом с дробью, которое "float64" округлил бы. "json.Number" хранит каждое из них точно, а замечание под выводом перечисляет каждое поле, для которого он выбран.
- Почему некоторые поля — указатели и что делает omitzero в тегах?
- Поле становится указателем там, где его ключ отсутствует в части объектов или в части из них равен null, чтобы nil мог сказать, что значение не пришло. "omitzero" ставится там, где ключ отсутствует в части объектов: тогда при повторном кодировании структуры этот ключ пропускается, как это было в вашем JSON, а не записывается как null.
- Почему json.RawMessage для ключа со значениями разных типов, а не any?
- "json.RawMessage" хранит байты каждого значения, пока ваша программа не решит, как их читать. "any" тоже принял бы эти значения, но число, декодированное в "any", — это "float64", и длинное целое теряет там свои последние цифры.
- Почему некоторые имена полей начинаются с X?
- Потому что иначе имя не начиналось бы с заглавной буквы: ключ записан в письменности без заглавных букв, начинается с цифры или начинается с буквы, у которой нет заглавной формы из одной буквы. X сохраняет в имени собственные буквы ключа, и поле по-прежнему читает свой ключ: X名前 читает "名前".
- Какая версия Go нужна структурам?
- Они написаны и проверены для Go 1.27. Проверяется и то, как теги читает Go 1.26, — через заменитель: Go 1.26 не читает ключ, содержащий символ за пределами букв, цифр, пробела ASCII и набора знаков пунктуации ASCII, например знак валюты или эмодзи, и страница называет каждое поле, чей ключ это затрагивает, — кроме ключа с буквой новее таблиц Unicode в Go 1.26, которого не видят ни страница, ни эта проверка.
- Почему одного из моих ключей нет в структуре?
- Потому что ключ содержит запятую, обратный слэш, двойную кавычку, апостроф или обратный апостроф либо пуст, а ни один тег структуры не может содержать такой ключ. Замечание перечисляет его по структуре и по самому ключу, например Root["a,b"]; остальной ваш JSON всё равно декодируется, а замечание говорит, как прочитать этот ключ другим способом.
- Безопасно ли вставлять ответ, в котором есть токены или пароли?
- Да. Каждый шаг выполняется внутри этой страницы на вашем компьютере, и ответ, который вы вставляете, никуда больше не уходит: его не получает ни один сервер, и ничто не хранит его копию. Объявления, которые из него получаются, содержат только имена полей, теги и типы Go, так что токен или пароль в JSON не оставляет в них никакого следа, кроме поля "string", названного по его ключу.
Похожие инструменты
- JSON в TypeScript
Там, где значения ключа бывают разных типов, эта страница хранит каждое из них так, как оно записано в вашем JSON, чтобы ваша программа его декодировала, ведь в Go нет union. Та страница пишет ту же форму в виде типов TypeScript под теми же именами типов, с union на этом месте, а её руководство объясняет, как эта форма вычисляется.
- JSON в Zod
В Go нет типа, который был бы просто числом JSON, поэтому там, где каждое число записано как целое, эта страница даёт полю целочисленный тип, и значение, записанное с десятичной точкой, в него не декодируется. Та страница пишет ту же форму в виде схем Zod под теми же именами типов, а схема проверяет данные каждый раз, когда они приходят, и принимает в таком поле и дробь.
- Тестер JSONPath
Проверка запросов JSONPath (RFC 9535) к JSON.
- Генератор таблиц Markdown
Создание и выравнивание таблиц Markdown из CSV, TSV или JSON.