В Go пакет encoding/json предлагает встроенные возможности маршалинга и демаршалинга JSON, что позволяет нам легко преобразовывать структуры данных Go в JSON и наоборот. Однако существуют сценарии, в которых поведение по умолчанию не работает, например при обработке сложных структур данных, управлении именами настраиваемых полей или работе со специальными типами данных. Цель этой статьи — дать представление о том, как можно обрабатывать JSON в Go, и, как всегда, с небольшой настройкой.

Отказ от ответственности: я склонен делать много ошибок каждый день. Если вы заметили один, я был бы более чем благодарен, если бы вы сообщили мне об этом.

Понимание маршалинга и демаршалинга JSON

JSON (JavaScript Object Notation) — это упрощенный формат обмена данными, широко используемый для связи между веб-службами и приложениями. Он представляет данные в виде пар ключ-значение, массивов и вложенных структур, обеспечивая простой и понятный формат для обмена данными.

В Go пакет encoding/json — это мощный инструмент для работы с данными JSON. Он предлагает функцию Marshal для преобразования структур данных Go в JSON и функцию Unmarshal для десериализации JSON в структуры данных Go. Эти функции автоматически обрабатывают большинство распространенных случаев, сопоставляя поля структуры Go со свойствами объекта JSON и наоборот. Однако при работе с более сложными сценариями или при необходимости точного управления процессом сериализации и десериализации лучше всего подходят теги JSON, а также пользовательские функции маршалинга и демаршалинга.

JSON-теги

То, как Go обрабатывает JSON, вращается вокруг структур и тегов полей. Структуры представляют собой структуру данных, подлежащую сериализации или десериализации, при этом каждое поле структуры соответствует свойству в представлении JSON. Теги полей, определенные с помощью тега структуры json, предоставляют метаданные, управляющие поведением сериализации и десериализации. Используя теги полей, мы можем указывать собственные имена для свойств JSON, обрабатывать нулевые значения, контролировать пропуск полей и многое другое. Давайте посмотрим на пример этих тегов:

type Person struct {
    Name      string    `json:"full_name"` 
    Age       int       `json:"-"` 
    Email     string    `json:"email,omitempty"` 
}
  • Name сопоставляется со свойством JSON full_name
  • Age будет полностью проигнорирован (не появится, несмотря ни на что)
  • Email будет отображаться в email, а omitempty — это зарезервированный флаг, который гарантирует, что если поле email пусто (например, содержит нулевое значение своего типа), оно не будет отображаться после сериализации в JSON.

Давайте посмотрим, как эти теги работают в действии:

pp := []Person{
    {
       Name:  "first person",
       Age:   20,
       Email: "[email protected]",
    },
    {
       Name:  "second person",
       Age:   25,
       Email: "",
    },
}

b, _ := json.MarshalIndent(pp, "", "\t")
fmt.Println(string(b))
[
    {
        "full_name": "first person",
        "email": "[email protected]"
    },
    {
        "full_name": "second person"
    }
]
  • Обратите внимание, что omitempty, если он используется исключительно как тег JSON, будет рассматриваться как имя поля, а не как флаг.
type Person struct {
    Name      string    `json:"omitempty"`
}

pp := []Person{
    {
       Name:  "first person",
    },
    {
       Name:  "second person",
    },
}

b, _ := json.MarshalIndent(pp, "", "\t")
fmt.Println(string(b))
[
    {
        "omitempty": "first person",
    },
    {
        "omitempty": "second person"
    }
]

ПРИМЕЧАНИЕ.Повторяющиеся имена не удастся автоматически сериализовать (например, нет ошибок, но ни одно из повторяющихся полей не появляется в выходных данных), хотя вы, вероятно, будете предупреждены инструмент статического анализа кода или линтер.

Теги JSON (или, скажем, любые пользовательские теги) можно получить во время выполнения с помощью reflection, как показано ниже:

func getFieldTags(t reflect.Type) map[string][]string {
   if t.Kind() != reflect.Struct {
      panic(fmt.Sprintf("t should be struct but is %s", t))
   }
  
   tags := make(map[string][]string)
   for i := 0; i < t.NumField(); i++ {
      f := t.Field(i)
      jTags := strings.SplitN(f.Tag.Get("json"), ",", -1)
      tags[f.Name] = jTags
   }
  
   return tags
}

func main() {
   tags := getFieldTags(reflect.TypeOf(Person{})) 
   fmt.Printf("%+v\n", tags) // map[Age:[-] Email:[email omitempty] Name:[full_name]]
}

Пользовательский маршалинг/демаршалинг JSON

Чтобы реализовать собственный маршалер/демаршалер JSON, вы должны соответственно реализовать интерфейсы json.Marshaler или json.Unmarshaler. Давайте посмотрим на эти интерфейсы:

// Marshaler is the interface implemented by types that
// can marshal themselves into valid JSON.
type Marshaler interface {
   MarshalJSON() ([]byte, error)
}

// Unmarshaler is the interface implemented by types
// that can unmarshal a JSON description of themselves.
// The input can be assumed to be a valid encoding of
// a JSON value. UnmarshalJSON must copy the JSON data
// if it wishes to retain the data after returning.
//
// By convention, to approximate the behavior of Unmarshal itself,
// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op.
type Unmarshaler interface {
   UnmarshalJSON([]byte) error
}

Допустим, нам нужно сделать вызов к какому-то удаленному API, чтобы получить информацию об определенных продуктах, и для примера предположим, что ответ API выглядит следующим образом:

{
    "products": [
        "prod A",
        "prod B",
        "prod C"
    ],
    "prices": [
        99.99,
        199.99,
        299.99
    ]
}

Мы знаем (и ожидаем), что одни и те же индексы содержат информацию о каком-то товаре, т.е. prices[0] принадлежит products[0]., чтобы лучше обрабатывать эти данные, мы хотим десериализовать (разобрать) ответ API в следующие структуры:

type Product struct {
   Name  string
   Price float64
}

type Cargo struct {
   Products []Product
}

Чтобы это работало, нам нужно объявить метод UnmarshalJSON для нашей целевой (здесь Cargo) структуры:

func main() {
   // our imaginary API
   d := GetProductPrices()
  
   cargo := &Cargo{}
   err := json.Unmarshal(d, cargo)
   if err != nil {
     log.Fatal("failed to unmarshal to cargo:", err)
   }

   fmt.Printf("%+v\n", cargo)
}

func (c *Cargo) UnmarshalJSON(d []byte) error {
   ...
}

Чтобы иметь точный контроль над тем, как наши данные десериализуются, мы можем временно разобрать их в любую желаемую промежуточную структуру:

func (c *Cargo) UnmarshalJSON(d []byte) error {

   // desired intermediary structure
   type temp struct {
    Products []string  `json:"products"`
    Prices   []float64 `json:"prices"`
   }

   tmp := &temp{}
   err := json.Unmarshal(d, tmp)
   if err != nil {
    return fmt.Errorf("failed to marshal JSON in temp: %w", err)
   }
   ...
}

Или мы можем даже использовать полностью расширяемую структуру данных, такую ​​как map[string]any :

func (c *Cargo) UnmarshalJSON(d []byte) error {

   // desired intermediary structure
   tmp := make(map[string]any)
   err := json.Unmarshal(d, &tmp)
   if err != nil {
    return fmt.Errorf("failed to marshal JSON in temp: %w", err)
   }
   ...
}

После того, как десериализованные данные готовы к дальнейшей обработке, следующим шагом будет преобразование их в любую структуру, которую мы хотим:

func (c *Cargo) UnmarshalJSON(d []byte) error {
   type temp struct {
      Products []string  `json:"products"`
      Prices   []float64 `json:"prices"`
   }
  
   tmp := &temp{}
   err := json.Unmarshal(d, tmp)
   if err != nil {
      return fmt.Errorf("failed to marshal JSON in temp: %w", err)
   }
  
   if len(tmp.Prices) != len(tmp.Products) {
      return fmt.Errorf("length of products (%d) and prices (%d) does not match", len(tmp.Products), len(tmp.Prices))
   }
  
   for i := 0; i < len(tmp.Products); i++ {
      c.Products = append(c.Products, Product{Name: tmp.Products[i], Price: tmp.Prices[i]})
   }
  
   return nil
}

Наконец, мы можем ожидать хорошо организованные данные, как показано ниже:

{
 "Products": [
  {
   "Name": "prod A",
   "Price": 100
  },
  {
   "Name": "prod B",
   "Price": 200
  },
  {
   "Name": "prod C",
   "Price": 300
  }
 ]
}

Пока я экспериментировал с этими вещами, у меня возник довольно интересный вопрос, который может волновать и вас прямо сейчас:

В случае, если наши структуры не реализуют интерфейсы json.Marshaler или json.Unmarshaler, Go по-прежнему находит способ сериализации и десериализации наших структур. Как это возможно?

Ну, под капотом, что действительно делает Go, так это то, что он смотрит, реализует ли данная структура какой-либо из этих интерфейсов. "Как?" Вы можете спросить. Ну и опять же все благодаря размышлениям:

// json/decode.go 
// check if a certain type implements "Unmarshaler" (v is reflect.Value)
if v.Type().NumMethod() > 0 && v.CanInterface() {
    if u, ok := v.Interface().(Unmarshaler); ok {
        return u, nil, reflect.Value{}
}

// json/encode.go
// check if a certain type implements "Marshaler" (t is reflect.Type)
marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem()
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {
    return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
}

В случае, если тип не проходит тест (например, не реализует ни json.Marshaler, ни json.Unmarshaler ), Go прибегает к чему-то вроде плана Б.

Честно говоря, я в основном использую пользовательские возможности mashaling/unmarshaling для постоянного изменения файлов JSON, встроенных в программу, сначала читая файл JSON, десериализовав его в любую структуру, которую я хочу, а затем сериализуя его обратно в новый файл .json. Что-то вроде этого:

type CustomStruct struct {
  // whatever field I want at the end
}

func (c *CustomStruct) UnmarshalJSON(d []byte) error {
  // custom JSON unmarshalling
}

func main() {
  // read, change, write
  d, err := os.ReadFile("old.json")
  if err != nil {
    log.Fatal("failed to read file:", err)
  }

  st := &CustomStruct{}
  err = json.Unmarshal(d, st)
  if err != nil {
    log.Fatal("failed to unmarshal:", err)
  }

  b, err := json.MarshalIndent(st, "", "\t")
  if err != nil {
    log.Fatal("failed to marshal struct:", err)
  }
 
  err = os.WriteFile("new.json", b, 0644)
  if err != nil {
    log.Fatal("failed to write new file:", err)
  }
}

Обратите внимание, что есть две важные вещи, которые я здесь не учел: первая — это чтение файла в потоковом режиме (поэтому я не забиваю себе память, читая файл целиком сразу). а другой — частичный демаршалинг в JSON. Возможно, мы рассмотрим эти темы позже.

Заключение

Пользовательская маршалинг и демаршалинг JSON в Go позволяет нам преодолеть ограничения возможностей обработки JSON по умолчанию, предоставляемых стандартной библиотекой. Используя настраиваемые маршалеры и демаршалеры, мы можем точно настроить процессы сериализации и десериализации, сделав наш код более выразительным, эффективным и адаптированным к конкретным случаям использования. Мы изучили различные аспекты пользовательской обработки JSON в Go, от методов реализации до реальных примеров. Надеюсь, вам понравилось наше путешествие. Спасибо за вашу поддержку заранее!