Эволюция JSON в Go: от v1 к v2

от автора

Вторая версия пакета json/v2, которая появится в Go 1.25 (август 2025) — большое обновление с множеством несовместимых изменений. В v2 добавили новые возможности, исправили ошибки в API и поведении, а также улучшили производительность. Давайте посмотрим, что изменилось!

Базовоый сценарий использования функций Marshal и Unmarshal не меняется. Этот код работает как в v1, так и в v2:

type Person struct {     Name string     Age  int } 
alice := Person{Name: "Alice", Age: 25}  // Кодируем Алису. b, err := json.Marshal(alice) fmt.Println(string(b), err)  // Декодируем Алису. err = json.Unmarshal(b, &alice) fmt.Println(alice, err) 
{"Name":"Alice","Age":25} <nil> {Alice 25} <nil> 

А вот остальное довольно сильно отличается. Давайте пройдемся по основным отличиям v2 по сравнению с v1.

MarshalWrite и UnmarshalRead

В v1 мы использовали Encoder, чтобы писать в io.Writer, и Decoder — чтобы читать из io.Reader:

// Кодируем Алису. alice := Person{Name: "Alice", Age: 25} out := new(strings.Builder) // io.Writer enc := json.NewEncoder(out) enc.Encode(alice) fmt.Println(out.String())  // Декодируем Боба. in := strings.NewReader(`{"Name":"Bob","Age":30}`) // io.Reader dec := json.NewDecoder(in) var bob Person dec.Decode(&bob) fmt.Println(bob) 
{"Name":"Alice","Age":25}  {Bob 30} 

Я пропускаю обработку ошибок, чтобы не усложнять примеры. Не делайте так в продакшене ツ

В v2 можно использовать MarshalWrite и UnmarshalRead напрямую, без посредников:

// Кодируем Алису. alice := Person{Name: "Alice", Age: 25} out := new(strings.Builder) json.MarshalWrite(out, alice) fmt.Println(out.String())  // Декодируем Боба. in := strings.NewReader(`{"Name":"Bob","Age":30}`) var bob Person json.UnmarshalRead(in, &bob) fmt.Println(bob) 
{"Name":"Alice","Age":25} {Bob 30 false} 

Но примеры не взаимозаменяемые:

  • MarshalWrite не добавляет перевод строки, в отличие от старого Encoder.Encode.

  • UnmarshalRead читает из ридера все подряд до io.EOF, а старый Decoder.Decode читает только следующее JSON-значение.

MarshalEncode и UnmarshalDecode

Типы Encoder и Decoder теперь находятся в новом пакете jsontext, и их интерфейсы сильно изменились (чтобы поддержать низкоуровневые операции потокового кодирования и декодирования).

Их можно использовать совместно с функциями пакета json, чтобы поточно читать и писать JSON, примерно как раньше работали Encode и Decode:

  • v1 Encoder.Encode → v2 json.MarshalEncode + jsontext.Encoder

  • v1 Decoder.Decode → v2 json.UnmarshalDecode + jsontext.Decoder

Поточный кодировщик:

people := []Person{     {Name: "Alice", Age: 25},     {Name: "Bob", Age: 30},     {Name: "Cindy", Age: 15}, } out := new(strings.Builder) enc := jsontext.NewEncoder(out)  for _, p := range people {     // Кодирует один объект Person за вызов.     json.MarshalEncode(enc, p) }  fmt.Print(out.String()) 
{"Name":"Alice","Age":25} {"Name":"Bob","Age":30} {"Name":"Cindy","Age":15} 

Поточный декодер:

in := strings.NewReader(`     {"Name":"Alice","Age":25}     {"Name":"Bob","Age":30}     {"Name":"Cindy","Age":15} `) dec := jsontext.NewDecoder(in)  for {     var p Person     // Декодирует один объект Person за вызов.     err := json.UnmarshalDecode(dec, &p)     if err == io.EOF {         break     }     fmt.Println(p) } 
{Alice 25} {Bob 30} {Cindy 15} 

В отличие от UnmarshalRead, функция UnmarshalDecode работает полностью в потоковом режиме — она декодирует по одному значению за каждый вызов, а не читает все сразу до io.EOF.

Опции

Опции настраивают нюансы поведения функций кодирования и декодирования:

  • FormatNilMapAsNull и FormatNilSliceAsNull определяют, как кодировать nil-карты и срезы.

  • MatchCaseInsensitiveNames сопоставляют имена без учета регистра, например, Namename.

  • Multiline записывает JSON-объекты в несколько строк.

  • OmitZeroStructFields убирает из результата поля со значением по умолчанию.

  • SpaceAfterColon и SpaceAfterComma добавляют пробел после : или ,.

  • StringifyNumbers записывает числа как строки.

  • WithIndent и WithIndentPrefix добавляют отступы для вложенных свойств (функция MarshalIndent в v2 удалена).

Каждая функция может принимать любое количество опций:

alice := Person{Name: "Alice", Age: 25} b, _ := json.Marshal(     alice,     json.OmitZeroStructFields(true),     json.StringifyNumbers(true),     jsontext.WithIndent("  "), ) fmt.Println(string(b)) 
{   "Name": "Alice",   "Age": "25" } 

Опции можно комбинировать с помощью JoinOptions:

alice := Person{Name: "Alice", Age: 25} opts := json.JoinOptions(     jsontext.SpaceAfterColon(true),     jsontext.SpaceAfterComma(true), ) b, _ := json.Marshal(alice, opts) fmt.Println(string(b)) 
{"Name": "Alice", "Age": 25} 

Полный список опций смотрите в документации: часть находится в пакете json, другие — в пакете jsontext.

Теги

v2 поддерживает теги полей из v1:

  • omitzero и omitempty — пропускать пустые значения.

  • string — записывать числа как строки.

  • - — игнорировать поля.

И добавляет еще несколько:

  • case:ignore и case:strict указывают, как обрабатывать различия в регистре.

  • format:template форматирует значение поля по шаблону.

  • inline делает вывод «плоским», встраивая поля вложенного объекта на уровень родителя.

  • unknown собирает все неизвестные поля в одно.

Вот пример для inline и format:

type Person struct {     Name string         `json:"name"`     // Форматировать дату как гггг-мм-дд.     BirthDate time.Time `json:"birth_date,format:DateOnly"`     // Встроить поля адреса в объект Person.     Address             `json:",inline"` }  type Address struct {     Street string `json:"street"`     City   string `json:"city"` }  func main() {     alice := Person{         Name: "Alice",         BirthDate: time.Date(2001, 7, 15, 12, 35, 43, 0, time.UTC),         Address: Address{             Street: "123 Main St",             City:   "Wonderland",         },     }     b, _ := json.Marshal(alice, jsontext.WithIndent("  "))     fmt.Println(string(b)) } 
{   "name": "Alice",   "birth_date": "2001-07-15",   "street": "123 Main St",   "city": "Wonderland" } 

И пример для unknown:

type Person struct {     Name string         `json:"name"`     // Собрать все неизвестные поля Person     // в поле Data.     Data map[string]any `json:",unknown"` }  func main() {     src := `{         "name": "Alice",         "hobby": "adventure",         "friends": [             {"name": "Bob"},             {"name": "Cindy"}         ]     }`     var alice Person     json.Unmarshal([]byte(src), &alice)     fmt.Println(alice) } 
{Alice map[friends:[map[name:Bob] map[name:Cindy]] hobby:adventure]} 

Собственные маршалеры

Как и раньше, можно задать собственную логику кодирования и декодирования, реализовав интерфейсы Marshaler и Unmarshaler. Этот код работает как в v1, так и в v2:

// Логический тип, в котором // true — это "✓", а false — "✗". type Success bool  func (s Success) MarshalJSON() ([]byte, error) {     if s {         return []byte(`"✓"`), nil     }     return []byte(`"✗"`), nil }  func (s *Success) UnmarshalJSON(data []byte) error {     // Валидация пропущена для краткости.     *s = string(data) == `"✓"`     return nil }  func main() {     // Кодируем true -> ✓.     val := Success(true)     data, err := json.Marshal(val)     fmt.Println(string(data), err)      // Декодируем ✓ -> true.     src := []byte(`"✓"`)     err = json.Unmarshal(src, &val)     fmt.Println(val, err) } 
"✓" <nil> true <nil> 

Однако, документация стандартной библиотеки советует использовать новые интерфейсы MarshalerTo и UnmarshalerFrom (они работают в потоковом режиме и могут быть намного быстрее):

// Логический тип, в котором // true — это "✓", а false — "✗". type Success bool  func (s Success) MarshalJSONTo(enc *jsontext.Encoder) error {     if s {         return enc.WriteToken(jsontext.String("✓"))     }     return enc.WriteToken(jsontext.String("✗")) }  func (s *Success) UnmarshalJSONFrom(dec *jsontext.Decoder) error {     // Валидация пропущена для краткости.     tok, err := dec.ReadToken()     *s = tok.String() == `"✓"`     return err }  func main() {     // Кодируем true -> ✓.     val := Success(true)     data, err := json.Marshal(val)     fmt.Println(string(data), err)      // Декодируем ✓ -> true.     src := []byte(`"✓"`)     err = json.Unmarshal(src, &val)     fmt.Println(val, err) } 
"✓" <nil> true <nil> 

Более того, вы больше не ограничены одним маршалером (кодировщиком) для конкретного типа. Теперь можно писать собственные маршалеры и анмаршалеры под конкретные ситуации — с помощью универсальных функций MarshalFunc и UnmarshalFunc.

func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers 

Например, можно кодировать значение bool в или без создания отдельного типа:

// Кодировщик для логических значений. boolMarshaler := json.MarshalFunc(     func(val bool) ([]byte, error) {         if val {             return []byte(`"✓"`), nil         }         return []byte(`"✗"`), nil     }, )  // Передаем кодировщик в Marshal // с помощью опции WithMarshalers. val := true data, err := json.Marshal(val, json.WithMarshalers(boolMarshaler)) fmt.Println(string(data), err) 
"✓" <nil> 

И декодировать или обратно в bool:

// Декодер для логических значений. boolUnmarshaler := json.UnmarshalFunc(     func(data []byte, val *bool) error {         *val = string(data) == `"✓"`         return nil     }, )  // Передаем декодер в в Unmarshal // через опцию WithUnmarshalers. src := []byte(`"✓"`) var val bool err := json.Unmarshal(src, &val, json.WithUnmarshalers(boolUnmarshaler)) fmt.Println(val, err) 
true <nil> 

Для создания собственных кодировщиков и декодеров предусмотрены также функции MarshalToFunc и UnmarshalFromFunc. Они похожи на MarshalFunc и UnmarshalFunc, но работают с jsontext.Encoder и jsontext.Decoder, а не с байтовыми срезами.

func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers 

Можно объединять маршалеры с помощью JoinMarshalers (и анмаршалеры с помощью JoinUnmarshalers). Например, вот как можно преобразовать логические значения (true/false) и «логические» строки (on/off) в значения /, сохранив при этом стандартное преобразование для всех остальных значений.

Сначала создаем маршалер для логических значений:

// Кодирует true/false в ✓/✗. boolMarshaler := json.MarshalToFunc(     func(enc *jsontext.Encoder, val bool) error {         if val {             return enc.WriteToken(jsontext.String("✓"))         }         return enc.WriteToken(jsontext.String("✗"))     }, ) 

Затем создаем маршалер для «логических» строк:

// Кодирует строки вида on/off в ✓/✗. strMarshaler := json.MarshalToFunc(     func(enc *jsontext.Encoder, val string) error {         if val == "on" || val == "true" {             return enc.WriteToken(jsontext.String("✓"))         }         if val == "off" || val == "false" {             return enc.WriteToken(jsontext.String("✗"))         }         // SkipFunc — специальная ошибка, которая инструктирует Go пропустить         // текущий маршалер и перейти к следующему. В нашем случае         // следующим будет стандартный маршалер для строк.         return json.SkipFunc     }, ) 

Наконец, объединяем кодировщики с помощью JoinMarshalers и передаем их в функцию маршалинга через опцию WithMarshalers:

// Объединяем маршалеры с помощью JoinMarshalers. marshalers := json.JoinMarshalers(boolMarshaler, strMarshaler)  // Кодируем в JSON несколько значений. vals := []any{true, "off", "hello"} data, err := json.Marshal(vals, json.WithMarshalers(marshalers)) fmt.Println(string(data), err) 
["✓","✗","hello"] <nil> 

Здорово, правда?

Поведение по умолчанию

В версии v2 изменился не только интерфейс пакета, но и поведение кодирования и декодирования по умолчанию.

Вот некоторые отличия в кодировании значений в JSON:

  • В v1 nil-срез кодируется как null, в v2 — как []. Настраивается опцией FormatNilSliceAsNull.

  • В v1 nil-карта кодируется как null, в v2 — как {}. Настраивается опцией FormatNilMapAsNull.

  • В v1 байтовый массив кодируется как массив чисел, в v2 — как base64-строка. Настраивается тегами format:array и format:base64.

  • В v1 допускаются некорректные UTF-8 символы в строке, в v2 — нет. Настраивается опцией AllowInvalidUTF8.

Вот пример умолчательного поведения v2:

type Person struct {     Name    string     Hobbies []string     Skills  map[string]int     Secret  [5]byte }  func main() {     alice := Person{         Name:    "Alice",         Secret: [5]byte{1, 2, 3, 4, 5},     }     b, _ := json.Marshal(alice, jsontext.Multiline(true))     fmt.Println(string(b)) } 
{     "Name": "Alice",     "Hobbies": [],     "Skills": {},     "Secret": "AQIDBAU=" } 

А так можно вернуть поведение v1:

type Person struct {     Name    string     Hobbies []string     Skills  map[string]int     Secret  [5]byte `json:",format:array"` }  func main() {     alice := Person{         Name:    "Alice",         Secret: [5]byte{1, 2, 3, 4, 5},     }     b, _ := json.Marshal(         alice,         json.FormatNilMapAsNull(true),         json.FormatNilSliceAsNull(true),         jsontext.Multiline(true),     )     fmt.Println(string(b)) } 
{     "Name": "Alice",     "Hobbies": null,     "Skills": null,     "Secret": [         1,         2,         3,         4,         5     ] } 

Вот некоторые отличия в декодировании значений из JSON:

  • В v1 имена полей сравниваются без учета регистра, в v2 — по точному совпадению. Настраивается опцией MatchCaseInsensitiveNames или тегом case.

  • В v1 допускается дублирование полей в объекте, в v2 — нет. Настраивается опцией AllowDuplicateNames.

Вот пример умолчательного поведения v2 (с учетом регистра):

type Person struct {     FirstName string     LastName  string }  func main() {     src := []byte(`{"firstname":"Alice","lastname":"Zakas"}`)     var alice Person     json.Unmarshal(src, &alice)     fmt.Printf("%+v\n", alice) } 
{FirstName: LastName:} 

А так можно вернуть поведение v1 (игнорировать регистр):

type Person struct {     FirstName string     LastName  string }  func main() {     src := []byte(`{"firstname":"Alice","lastname":"Zakas"}`)     var alice Person     json.Unmarshal(         src, &alice,         json.MatchCaseInsensitiveNames(true),     )     fmt.Printf("%+v\n", alice) } 
{FirstName:Alice LastName:Zakas} 

Полный список изменений в поведении смотрите в документации.

Производительность

При кодировании v2 работает примерно так же, как v1. С некоторыми датасетами быстрее, с другими — медленнее. Но при декодировании разница большая: v2 быстрее v1 в 3–10 раз.

Также можно значительно повысить производительность, если вместо обычных MarshalJSON и UnmarshalJSON использовать их потоковые аналоги — MarshalJSONTo и UnmarshalJSONFrom. По словам команды Go, это позволяет снизить сложность некоторых рантайм-сценариев с O(n²) до O(n). Например, переход с UnmarshalJSON на UnmarshalJSONFrom для OpenAPI-спецификации Kubernetes ускорил процесс примерно в 40 раз.

Подробности бенчмарков — в репозитории jsonbench.

Заключение

Уф! Неслабый объем изменений. Пакет v2 более фичастый и гибкий, чем v1 — но он и намного сложнее, особенно из-за разделения на пакеты json/v2 и jsontext.

Пара моментов, которые стоит учитывать:

  • В Go 1.25 пакет json/v2 считается экспериментальным. Его можно включить через переменную GOEXPERIMENT=jsonv2 во время сборки. API пакета может измениться в будущих версиях.

  • Если включить GOEXPERIMENT=jsonv2, то старый пакет json будет использовать новую реализацию «под капотом».

А вы что думаете о json/v2?

P.S. Если вам интересен Go, приглашаю подписаться на мой канал Thank Go. Там, кстати, разбираем и все остальные изменения грядущей версии 1.25.


ссылка на оригинал статьи https://habr.com/ru/articles/923404/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *