В Go пакет encoding/json
предлагает встроенные возможности маршалинга и демаршалинга JSON, что позволяет нам легко преобразовывать структуры данных Go в JSON и наоборот. Однако существуют сценарии, в которых поведение по умолчанию не работает, например при обработке сложных структур данных, управлении именами настраиваемых полей или работе со специальными типами данных. Цель этой статьи — дать представление о том, как можно обрабатывать JSON в Go, и, как всегда, с небольшой настройкой.
Отказ от ответственности: я склонен делать много ошибок каждый день. Если вы заметили один, я был бы более чем благодарен, если бы вы сообщили мне об этом.
- Все фрагменты кода можно найти в моем репозитории Github.
Понимание маршалинга и демаршалинга 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
сопоставляется со свойством JSONfull_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, от методов реализации до реальных примеров. Надеюсь, вам понравилось наше путешествие. Спасибо за вашу поддержку заранее!