Вторая версия пакета 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.Encoder
v1
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.
Комментарии (2)
Stanislavvv
30.06.2025 16:30Интересно, есть ли более-менее работающий с xml аналог питоновского BeautifulSoap? С кривыми fb2 весьма помогает, но в питоне.
edo1h
А в сравнении с go-json как?
update: нашёл, если в структуру, то не догнал, но разрыв сильно сократился:
Relative to JSONv1, JSONv2 is 2.7x to 10.2x faster.
Relative to GoJSON, JSONv2 is 1.3x to 1.8x slower.