Анализ изменений в поведении encoding/json/v2 в Go 1.25 и слоя совместимости с v1

Пакет encoding/json v2 в Go представляет собой новую реализацию, призванную устранить несколько недостатков существующей версии v1: непоследовательность, неожиданное поведение и проблемы производительности. Она активируется через build tag goexperiment.jsonv2 и является экспериментальной функцией.
Самое важное: когда v2 включен, v1 работает как слой совместимости, который эмулирует поведение v1 поверх реализации v2. Это реализовано через функцию DefaultOptionsV1() в файле v2_v1.go. Иными словами, v2 предоставляет параметры, с помощью которых можно полностью воспроизвести поведение v1, и одновременно предлагает более строгое и предсказуемое поведение по умолчанию.
Основные цели v2 следующие:
- Повышение корректности и предсказуемости: по умолчанию применяются более строгие правила (например, учет регистра и запрет дублирующихся ключей), что снижает количество неожиданного поведения.
- Повышение производительности: движки парсинга и кодирования переработаны для повышения эффективности.
- Расширение гибкости и контроля: введена подробная система параметров, позволяющая разработчикам детально управлять обработкой 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опускается, тогда как в v2falseявляется непустым 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().