Вторая версия пакета 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→ v2json.MarshalEncode+jsontext.Encoderv1
Decoder.Decode→ v2json.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сопоставляют имена без учета регистра, например,Name↔name.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.
