---
id: 61f2009278a5f7366f79395da59447cf
author: lemonmint
title: Анализ изменений в поведении encoding/json/v2 в Go 1.25 и слоя совместимости с v1
description: Сравниваются сопоставление с учётом регистра, изменение семантики omitempty и omitzero, сериализация nil-срезов и nil-отображений, а также параметры encoding/json/v2, введённого как экспериментальная функция Go 1.25 (goexperiment.jsonv2).
language: ru
date: 2025-06-20T05:38:20.100520237Z
path: /blog/posts/go-1-25-encoding-json-v1-vs-v2-z74a36b08
category: release
---

Пакет `encoding/json` v2 в Go представляет собой новую реализацию, призванную устранить несколько недостатков существующей версии v1: непоследовательность, неожиданное поведение и проблемы производительности. Она активируется через build tag `goexperiment.jsonv2` и является экспериментальной функцией.

Самое важное: **когда v2 включен, v1 работает как слой совместимости, который эмулирует поведение v1 поверх реализации v2**. Это реализовано через функцию `DefaultOptionsV1()` в файле `v2_v1.go`. Иными словами, v2 предоставляет параметры, с помощью которых можно полностью воспроизвести поведение v1, и одновременно предлагает более строгое и предсказуемое поведение по умолчанию.

Основные цели v2 следующие:

1. **Повышение корректности и предсказуемости**: по умолчанию применяются более строгие правила (например, учет регистра и запрет дублирующихся ключей), что снижает количество неожиданного поведения.
2. **Повышение производительности**: движки парсинга и кодирования переработаны для повышения эффективности.
3. **Расширение гибкости и контроля**: введена подробная система параметров, позволяющая разработчикам детально управлять обработкой JSON.

---

## Основные различия в семантике и поведении

Ниже перечислены различия между v1 и v2 по пунктам, с опорой на файл `v2_test.go`.

### 1. Сопоставление имен полей (учет регистра)

- **Поведение v1**: при демаршалинге членов JSON-объекта в поля структуры Go сопоставление выполняется **без учета регистра (case-insensitive)**. И `"FirstName"`, и `"firstname"` сопоставляются с полем `FirstName`.
- **Поведение v2**: по умолчанию сопоставляются только поля, имена которых **точно совпадают с учетом регистра (case-sensitive)**.
- **Причина изменения**: сопоставление без учета регистра может быть источником неожиданного поведения и замедляет обработку несовпадающих полей. v2 принимает более ясное и предсказуемое поведение по умолчанию.
- **Связанные параметры**: в v2 можно явно включить игнорирование регистра для отдельного поля с помощью тега `json:"...,case:ignore"` или глобально применить опцию `json.MatchCaseInsensitiveNames(true)`.

### 2. Изменение семантики опции `omitempty`

- **Поведение v1**: поле опускается на основании «пустого состояния» Go-значения. Под «пустым состоянием» понимаются `false`, `0`, `nil` для указателей и интерфейсов, а также массивы, срезы, отображения и строки нулевой длины.
- **Поведение v2**: поле опускается на основании «пустого состояния» закодированного JSON-значения. То есть поле опускается, если оно кодируется как `null`, `""`, `{}` или `[]`.
- **Причина изменения**: определение v1 зависит от системы типов Go. v2 опирается на систему типов JSON и обеспечивает более согласованное поведение. Например, в v1 значение `false` для типа `bool` опускается, тогда как в v2 `false` является непустым JSON-значением и не опускается. В v2 добавлена опция `omitzero`, которая может заменить поведение v1, при котором `omitempty` применялся к `0` или `false`.
- **Связанные параметры**: чтобы получить поведение, идентичное v1, используйте опцию `json.OmitEmptyWithLegacyDefinition(true)`.

### 3. Изменение поведения опции `string`

- **Поведение v1**: применяется к полям числового, логического типов и типа строки. Значение повторно кодируется внутри JSON-строки (например, `int(42)` -> `"42"`). К значениям внутри составных типов (срезы, отображения и т. д.) рекурсивно не применяется.
- **Поведение v2**: **применяется только к числовым типам** и применяется рекурсивно. То есть числа внутри срезов, например `[]int`, также кодируются как JSON-строки.
- **Причина изменения**: основное назначение опции `string` состоит в том, чтобы представлять числа в виде строк и предотвращать потерю точности 64-битных целых чисел. Поведение v1 было ограниченным и непоследовательным. v2 сосредоточен на этом основном назначении и расширяет поведение рекурсивно, делая опцию более полезной.
- **Связанные параметры**: опция `json.StringifyWithLegacySemantics(true)` позволяет воспроизвести поведение v1.

### 4. Маршалинг `nil`-срезов и отображений

- **Поведение v1**: `nil`-срезы и `nil`-отображения маршалируются как `null`.
- **Поведение v2**: по умолчанию `nil`-срезы маршалируются как `[]` (пустой массив), а `nil`-отображения как `{}` (пустой объект).
- **Причина изменения**: `nil` является деталью реализации языка Go, и раскрывать её в формате JSON, не зависящем от языка, нежелательно. Для представления пустых коллекций более распространены `[]` и `{}`.
- **Связанные параметры**: в v2 можно получить поведение v1 и маршалировать как `null` с помощью опций `json.FormatNilSliceAsNull(true)` или `json.FormatNilMapAsNull(true)`.

### 5. Демаршалинг массивов

- **Поведение v1**: при демаршалинге в массив Go (`[N]T`) ошибка не возникает, даже если длина JSON-массива отличается от длины массива Go. Если JSON-массив короче, оставшиеся элементы заполняются нулевыми значениями; если длиннее, лишние элементы отбрасываются.
- **Поведение v2**: длина JSON-массива должна **точно совпадать** с длиной массива Go. В противном случае возникает ошибка.
- **Причина изменения**: в Go длина массива фиксированного размера часто имеет важное значение. Поведение v1 может приводить к тихой потере данных. v2 использует более строгие правила, повышая корректность.
- **Связанные параметры**: опция `json.UnmarshalArrayFromAnyLength(true)` позволяет воспроизвести поведение v1.

### 6. Обработка `time.Duration`

- **Поведение v1**: `time.Duration` внутренне рассматривается как `int64` и кодируется как JSON-число в наносекундах.
- **Поведение v2**: кодируется как JSON-строка в формате, возвращаемом методом `time.Duration.String()`, например `"1h2m3s"`.
- **Причина изменения**: числа в наносекундах плохо читаются, а стандартное строковое представление `time.Duration` более полезно.
- **Связанные параметры**: поведение v1 можно использовать с помощью тега `json:",format:nano"` или опции `json.FormatTimeWithLegacySemantics(true)`.

### 7. Обработка недопустимого UTF-8

- **Поведение v1**: при маршалинге и демаршалинге недопустимые байты UTF-8 внутри строк **молча заменяются** символом замены Unicode (`\uFFFD`).
- **Поведение v2**: по умолчанию при обнаружении недопустимого UTF-8 **возвращается ошибка**.
- **Причина изменения**: это позволяет предотвратить тихое повреждение данных и соответствовать более строгому стандарту JSON (RFC 7493).
- **Связанные параметры**: поведение v1 можно воспроизвести с помощью опции `jsontext.AllowInvalidUTF8(true)`.

### 8. Обработка повторяющихся имен членов объекта

- **Поведение v1**: в JSON-объекте допускаются повторяющиеся члены с одинаковым именем. Побеждает значение, указанное последним, которое перезаписывает предыдущие.
- **Поведение v2**: по умолчанию при наличии повторяющихся имен членов **возвращается ошибка**.
- **Причина изменения**: RFC 8259 не определяет поведение при повторяющихся именах, поэтому разные реализации могут вести себя по-разному. Это может стать причиной уязвимостей безопасности. v2 явно отклоняет такие данные, повышая корректность и безопасность.
- **Связанные параметры**: поведение v1 можно воспроизвести с помощью опции `jsontext.AllowDuplicateNames(true)`.

---

## Различия в реализации и архитектуре

- **v1**: в значительной степени опирается на `decodeState` в `decode.go` и вручную написанный конечный автомат в `scanner.go`. Это монолитная структура, в которой логика парсинга тесно связана с семантическим анализом.
- **v2**: архитектура стала более модульной.
  - **`encoding/json/jsontext`**: предоставляет низкоуровневые высокопроизводительные токенизатор (`Decoder`) и кодировщик (`Encoder`) JSON. Этот пакет сосредоточен исключительно на синтаксической стороне JSON.
  - **`encoding/json/v2`**: на основе `jsontext` выполняет семантическое преобразование между типами Go и значениями JSON.
  - Такое разделение отделяет синтаксический анализ от семантического, что повышает ясность кода и производительность.

---

## Новый API и возможности v2

v2 предоставляет очень гибкие средства управления через систему `json.Options`.

- **`json.Options`**: набор параметров, изменяющих поведение маршалинга и демаршалинга.
- **`json.JoinOptions(...)`**: объединяет несколько параметров в один.
- **`WithMarshalers` / `WithUnmarshalers`**: мощная возможность внедрять логику сериализации и десериализации для конкретных типов без реализации интерфейсов `Marshaler`/`Unmarshaler`. Это особенно полезно при работе с типами из внешних пакетов.
- **Новые параметры**: `RejectUnknownMembers`, `Deterministic(false)`, `FormatNilSliceAsNull` и другие обеспечивают управление поведением, которое было невозможно в v1.

---

## Заключение

`encoding/json` v2 является современной реализацией, которая на основе опыта v1 существенно повышает **корректность, производительность и гибкость**. Поведение по умолчанию стало строже, однако с помощью развитой системы `Options` можно полностью воспроизвести все поведение v1, поэтому можно сохранять совместимость с существующим кодом и постепенно внедрять преимущества v2.

- Для **новых проектов** рекомендуется использовать v2 по умолчанию.
- **Существующие проекты** могут продолжать использовать `jsonv1` без изменений либо мигрировать на `jsonv2`, постепенно внедряя строгое поведение v2 через `DefaultOptionsV1()`.
