Все статьи
Руководства

JSON в Go структуры: генерация типов

Конвертация JSON в Go struct, теги json, вложенные структуры, использование в Go API.

20 апреля 2025
7 мин чтения
ConvertHub
#go#golang#json

Введение

Go — статически типизированный язык, и работа с JSON в нём организована через структуры (struct). Вместо того чтобы вручную описывать поля, теги и типы, разработчики часто генерируют struct прямо из примера JSON-ответа API. Это экономит время на старте интеграции, снижает риск опечаток в тегах и помогает увидеть структуру данных до написания логики. В статье разберём, как Go работает с JSON, как правильно составлять теги, что делать с вложенными объектами и массивами, и какие инструменты помогают автоматизировать генерацию. Для разовой конвертации используйте нашконвертер JSON в Go struct — вставили данные, получили готовый код.

Как Go работает с JSON

В стандартной библиотеке за сериализацию отвечает пакетencoding/json. Главная функцияjson.Marshal превращает значение Go в[]byte, а json.Unmarshal — обратно. Структуры играют роль схемы: имя поля в Go по умолчанию совпадает с ключом в JSON, но через тег json:"..."можно задать произвольное имя.

package main

import (
  "encoding/json"
  "fmt"
)

type User struct {
  ID    int    `json:"id"`
  Name  string `json:"name"`
  Email string `json:"email"`
}

func main() {
  raw := []byte(`{"id":42,"name":"Алиса","email":"a@example.com"}`)
  var u User
  _ = json.Unmarshal(raw, &u)
  fmt.Printf("%+v\n", u)
}

Теги json: зачем и как

Тег json:"name" указывает, под каким ключом поле ищется в JSON. Без тега Go использовал бы имя поля как есть, что часто не совпадает с JSON-стилемsnake_case или camelCase. Кроме имени, в теге можно задать поведение:

  • json:"name,omitempty" — опустить поле при маршалинге, если оно пустое;
  • json:"-,omitempty" — пропустить поле полностью;
  • json:"name,string" — маршалить число как строку;
  • json:"," — только поведение, имя поля берётся из Go-имени.

Типы данных: соответствие JSON и Go

Каждому типу JSON соответствует свой тип в Go. Знание этой таблицы помогает читать сгенерированные struct и понимать, что окажется в полях.

JSONGoПримечание
stringstringUTF-8 по умолчанию
numberfloat64Без тега string
numberint, int64Если значение целое
booleanbool
objectstructВложенная структура
array[]TСрез элементов
nullnil / *TУказатель для различения null и нуля
interfaceЛюбой тип, если структура неизвестна

Целые числа и float64

По умолчанию генераторы создают поля типа float64, потому что в JSON нет различия между целыми и дробными числами. Если вы уверены, что поле всегда целое — замените наint64 или int. Для денежных сумм используйте json.Number или сторонний типdecimal.Decimal, иначе возможна потеря точности.

Вложенные объекты и массивы

Вложенные объекты становятся отдельными структурами, массивы — срезами. Если вложенный объект встречается только внутри родителя, его можно объявить анонимно прямо в поле. Если же структура используется в нескольких местах — вынесите её отдельно и дайте осмысленное имя.

type Order struct {
  ID       int       `json:"id"`
  Customer Customer  `json:"customer"`
  Items    []Item    `json:"items"`
  Total    float64   `json:"total"`
  Created  time.Time `json:"created_at"`
}

type Customer struct {
  ID    int    `json:"id"`
  Name  string `json:"name"`
  Email string `json:"email,omitempty"`
}

type Item struct {
  SKU      string  `json:"sku"`
  Quantity int     `json:"quantity"`
  Price    float64 `json:"price"`
}

Поле time.Time работает «из коробки» — пакетencoding/json понимает RFC 3339 timestamps. Если API возвращает дату в другом формате, добавьте пользовательский тип с методами MarshalJSON иUnmarshalJSON.

Массивы примитивов и объектов

Массивы строк превращаются в []string, массивы чисел — в []float64 или []int, массивы объектов — в срезы структур. Если элемент массива может быть разнотипным (например, ответ GraphQL с разными узлами), берите[]interface или []json.RawMessage и разбирайте каждый элемент отдельно.

Обработка nullable и опциональных полей

В JSON поля могут отсутствовать, быть null или содержать значение. В Go три этих состояния неразличимы для обычных типов: нулевое значение нуля для числа — это и «отсутствует», и «null», и «0». Чтобы различать их, применяют указатели:

type Profile struct {
  Login string  `json:"login"`
  Age   *int    `json:"age"`        // nil, если поле отсутствует или null
  Bio   *string `json:"bio,omitempty"`
}

Теперь можно безопасно проверить if p.Age != nil и обратиться к значению через *p.Age. Генераторы по умолчанию не ставят указатели — это решение принимает разработчик, опираясь на семантику поля.

Автоматическая генерация struct

Ручное переписывание полей из примера JSON занимает минуты, но на крупных API с десятками эндпоинтов накапливается в часы. Существуют два подхода к автоматизации: CLI-утилиты и онлайн-конвертеры.

Онлайн-конвертеры

Самый быстрый путь — вставить JSON в поле ввода и получить готовый код. Наш конвертер JSON в Go struct умеет:

  • определять типы полей по значениям;
  • расставлять теги json:"..." со snake_case;
  • выносить вложенные объекты в именованные struct;
  • превращать массивы в срезы;
  • опционально добавлять omitempty или указатели.

Сгенерированный код можно сразу скопировать в проект. Это удобно на этапе прототипа: получили ответ API — за секунду получили типы — начали писать бизнес-логику.

CLI и инструменты разработки

Для командной работы и CI используют утилитуjson-to-struct из пакетаgithub.com/a-h/generate или популярныйgo-jsonschema. Последний принимает JSON Schema, а не пример, и генерирует строгие типы с валидацией. Если у вас OpenAPI-спецификация — посмотрите на oapi-codegenили swag, они создают не только модели, но и серверные/клиентские интерфейсы.

# Установка generate
go install github.com/a-h/generate/cmd/generate@latest

# Генерация из JSON-файла
generate -i response.json -o models.go --package models

Практические советы

Имена структур и полей

Генераторы дают технические имена: AutoGenerated,User1, InlineObject. Перед коммитом переименуйте их в осмысленные: User,OrderItem, Address. Имена полей в Go принято писать в CamelCase, а теги — вsnake_case или camelCase, в зависимости от конвенций API.

Одинаковые структуры — общий тип

Если в разных эндпоинтах встречается одинаковое представление сущности (например, короткая карточка пользователя), не плодите копии. Создайте один тип UserBrief и используйте его во всех местах. Это упростит рефакторинг и валидацию.

Проверка на реальных данных

После генерации прогоните через json.Unmarshalнесколько реальных ответов API, включая пограничные случаи: пустые массивы, null вместо объекта, отсутствующие поля. Если где-то падает cannot unmarshal — добавьте указатель или interface для проблемного поля.

Совместимость с изменениями API

API эволюционируют: добавляются поля, меняются типы. Чтобы не ломать существующий код, добавляйте новые поля как опциональные с omitempty. Для удаления поля оставьте его в struct на один релиз, пометив как deprecated, и только потом убирайте. Это даёт потребителям время на миграцию.

Кастомные маршаллеры и сложные типы

Не все типы JSON удобно представляют стандартными полями. Время в нестандартном формате, деньги в строковом виде с запятой, перечисления как строки — для этих случаев реализуют интерфейсы json.Marshaler иjson.Unmarshaler на пользовательском типе. Генераторы такое не делают, но код легко расширить вручную.

type Money int64 // в копейках

func (m Money) MarshalJSON() ([]byte, error) {
  return []byte(fmt.Sprintf("\"%d.%02d\"", m/100, m%100)), nil
}

func (m *Money) UnmarshalJSON(data []byte) error {
  var s string
  if err := json.Unmarshal(data, &s); err != nil {
    return err
  }
  parts := strings.Split(s, ".")
  rubles, _ := strconv.ParseInt(parts[0], 10, 64)
  kopecks := int64(0)
  if len(parts) > 1 {
    kopecks, _ = strconv.ParseInt(parts[1], 10, 64)
  }
  *m = Money(rubles*100 + kopecks)
  return nil
}

После этого поле типа Money в struct будет автоматически маршалиться в строку "123.45" и обратно — без дополнительной логики в бизнес-коде.

Версионирование и обратная совместимость

При эволюции API удобно хранить в одной структуре поля нескольких версий и размечать их через теги. Старые поля помечают как deprecated в комментарии, новые добавляют сomitempty. Парсинг tolerate-стилем — отсутствующие поля не ломают десериализацию, а лишние поля в JSON по умолчанию игнорируются.

type User struct {
  ID       int    `json:"id"`
  // Deprecated: использовать Email
  Login    string `json:"login,omitempty"`
  Email    string `json:"email,omitempty"`
  Phone    string `json:"phone,omitempty"` // добавлено в v2
}

Заключение

Генерация Go struct из JSON — это и экономия времени, и способ быстро понять форму данных. Стандартный пакетencoding/json покрывает базовые сценарии, тегиjson:"..." управляют именами и поведением полей, указатели помогают различать null и нулевое значение. Для разовых задач удобнее всегоонлайн-конвертер JSON в Go struct, для повторяющихся в командной работе — CLI-утилиты в связке с JSON Schema или OpenAPI. Главное — не оставлять автоматически сгенерированный код «как есть»: переименуйте структуры в осмысленные, добавьте указатели для nullable полей и проверьте на реальных ответах API.

Попробуйте эти инструменты

Похожие статьи