golang

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

  • вторник, 1 июля 2025 г. в 00:00:07
https://habr.com/ru/articles/923404/

Вторая версия пакета 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.