GoSuda

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

By lemonmint views ...

Пакет 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().