Menu

JSON в Golang: Marshal, Unmarshal и теги структур

Как кодировать и декодировать JSON в Go через encoding/json: Marshal и Unmarshal, теги структур вроде omitempty и omitzero, красивый вывод, декодирование в map[string]any, отказ от неизвестных полей и потоковая обработка через Decoder.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Marshal и Unmarshal

json.Marshal превращает значение Go в байты JSON. json.Unmarshal заполняет значение Go из байтов JSON. Теги структур выбирают имена полей JSON.

Email нет в первом выводе из-за omitempty и пустой строки. Unmarshal нужен указатель (&back); если передать само значение, вернётся InvalidUnmarshalError.

Теги структур

Синтаксис тега: json:"name,option,option".

ТегЭффект
json:"user_id"использовать ключ user_id
json:"email,omitempty"не выводить, если false, 0, "", nil, пустой слайс или мапа
json:",omitzero" (Go 1.24)не выводить, если значение нулевое или его IsZero() возвращает true
json:"-"никогда не кодировать и не декодировать это поле
json:"-,"использовать буквальный ключ -
json:"count,string"кодировать число или bool как строку JSON ("42")
без тегаключ совпадает с именем поля Go, UserID

Пароль никогда не покидает структуру, баланс выводится в кавычках, а deleted_at пропадает, тогда как created_at печатает нулевое время 0001-01-01T00:00:00Z. Именно из-за этой разницы и добавили omitzero: omitempty никогда не работал для структур, и с time.Time это давний сюрприз. На Go 1.23 и раньше используйте *time.Time с omitempty, чтобы получить тот же эффект.

MarshalIndent(v, prefix, indent) даёт читаемый вывод. Используйте его для файлов конфигурации и отладки; API обычно отправляют компактный JSON.

Только экспортированные поля

encoding/json использует рефлексию и видит только экспортированные поля. Это самый частый баг с JSON в Go:

type point struct {
	x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}

Ни ошибки, ни предупреждения, просто {} на выходе и поля, оставшиеся нулевыми, на входе. Пишите имена полей с заглавной буквы и добавляйте теги для ключей в нижнем регистре.

Как декодирование сопоставляет поля

При декодировании в структуру:

  • Ключи сопоставляются с именем из тега или с именем поля без учёта регистра. {"NAME": "x"} заполнит поле с тегом json:"name".
  • Ключи без подходящего поля молча игнорируются.
  • Поля без подходящего ключа сохраняют текущее значение. Unmarshal их не сбрасывает, поэтому декодирование в структуру, где уже есть данные, сливается с ними.
  • Несовпадение типов (строка там, где в структуре int) возвращает *json.UnmarshalTypeError, но остальные поля всё равно заполняются.

Чтобы отвергать неожиданные ключи, например в строгом API или в файле конфигурации с опечатками, используйте Decoder с DisallowUnknownFields:

Неизвестная структура: map[string]any

Когда форма данных заранее неизвестна, декодируйте в map[string]any или any. Соответствие фиксированное:

JSONGo
объектmap[string]any
массив[]any
строкаstring
числоfloat64
true / falsebool
nullnil

В выводе видны две ловушки. Каждое число это float64, поэтому m["stock"].(int) не сработает. А целые больше 2^53 теряют точность как float64: id 9007199254740993 возвращается как ...992. UseNumber избавляет от обеих.

Если часть структуры известна, декодируйте эту часть в структуру, а для остального используйте json.RawMessage. Он хранит сырые байты поля, чтобы вы могли декодировать его позже, когда узнаете тип.

Слайсы, мапы, указатели и nil

Значение GoJSON
nil-слайс (var s []int)null
пустой слайс ([]int{})[]
nil-мапаnull
nil-указательnull
[]byteстрока в base64
map[string]Tобъект, ключи отсортированы
map[int]Tобъект с целыми ключами в виде строк

Разница между nil и пустым слайсом важна для клиентов API, которые ждут массив. Инициализируйте через []T{} или make([]T, 0), когда поле должно быть [].

Используйте поле-указатель (*int, *bool), когда на входе нужно отличать «отсутствует» от «ноль». После декодирования nil-указатель означает, что ключа не было или он равен null; указатель на 0 означает, что клиент прислал 0.

Потоки: Encoder и Decoder

json.Marshal и Unmarshal работают со слайсами байтов целиком. Для io.Reader или io.Writer (тело HTTP, файл, stdin) используйте json.NewDecoder и json.NewEncoder. Decoder умеет также читать последовательность значений JSON по одному:

Encoder.Encode пишет перевод строки после каждого значения. По умолчанию и Marshal, и Encoder экранируют <, > и & как \u003c, \u003e и \u0026, чтобы JSON можно было безопасно вставлять в HTML. SetEscapeHTML(false) это отключает. В HTTP-обработчиках обычная пара это json.NewDecoder(r.Body).Decode(&v) и json.NewEncoder(w).Encode(v).

Собственное кодирование

Тип может сам управлять своим JSON, реализовав json.Marshaler и json.Unmarshaler. Частый случай: перечисление через iota, которое должно выглядеть как имя, а не число:

У MarshalJSON получатель-значение, поэтому он работает и для значений, и для указателей; UnmarshalJSON нужен получатель-указатель, потому что он меняет значение. Типы, которым нужна только строковая форма, могут вместо этого реализовать encoding.TextMarshaler (MarshalText), и тогда их можно использовать ещё и как ключи мап.

Частые ошибки

  • Имена полей со строчной буквы. Они молча пропускаются.
  • Передача в Unmarshal значения. Нужен указатель.
  • Игнорирование ошибки. О некорректном JSON и несовпадении типов сообщает только она.
  • Расчёт на то, что числа в map[string]any это int. Они float64.
  • Ожидание, что omitempty уберёт пустую структуру или нулевой time.Time. Используйте omitzero на Go 1.24 или указатель.
  • Расчёт на порядок полей в мапах. Ключи мап на выходе сортируются; поля структур сохраняют порядок объявления.

Часто задаваемые вопросы

Как преобразовать структуру в JSON в Go?

Вызовите json.Marshal(v), которая возвращает []byte и ошибку. Попадают туда только экспортированные поля (имена с заглавной буквы). Чтобы задать ключ JSON, используйте на поле тег вроде json:"name". Для вывода с отступами используйте json.MarshalIndent(v, "", " ").

Почему поля моей структуры отсутствуют в JSON?

encoding/json видит только экспортированные поля. Поле с именем name (со строчной буквы) для него невидимо и при кодировании, и при декодировании. Напишите имя поля с заглавной буквы и задайте ключ JSON тегом вроде json:"name".

Что делает omitempty в JSON в Go?

omitempty не выводит поле, когда в нём пустое значение: false, 0, "", nil-указатель или интерфейс, пустой слайс или мапа. Структуру или time.Time он пустыми не считает. В Go 1.24 появился omitzero, который опускает любое значение, равное нулевому значению своего типа (или чей метод IsZero() возвращает true), включая структуры и time.Time.

Как разобрать JSON с неизвестной структурой в Go?

Декодируйте в map[string]any (или в any). Объекты становятся map[string]any, массивы []any, строки string, булевы значения bool, а любые числа float64. Читайте значения через утверждения типа, а если большие целые должны остаться точными, используйте Decoder.UseNumber.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ