Команда Go for Devs подготовила перевод статьи о новом экспериментальном API для работы с JSON в Go. Спустя почти 15 лет после появления encoding/json
в стандартной библиотеке разработчики столкнулись с его ограничениями. В версии Go 1.25 появился экспериментальный encoding/json/v2
— он решает старые проблемы, добавляет потоковую обработку и повышает производительность.
Введение
JavaScript Object Notation (JSON) — это простой формат обмена данными. Почти 15 лет назад мы написали о поддержке JSON в Go, добавив возможность сериализовать и десериализовать Go-типы в JSON и обратно. С тех пор JSON стал самым популярным форматом данных в Интернете. Go-программы повсеместно читают и записывают JSON, а пакет encoding/json
сейчас занимает 5-е место среди самых импортируемых пакетов Go.
Со временем пакеты развиваются под потребности пользователей, и encoding/json
не стал исключением. Этот пост посвящён новым экспериментальным пакетам Go 1.25 — encoding/json/v2
и encoding/json/jsontext
, которые приносят долгожданные улучшения и исправления. В статье объясняется, зачем нужен новый основной вариант API, даётся обзор новых пакетов и рассказывается, как ими воспользоваться. Экспериментальные пакеты не видны по умолчанию и могут измениться в будущих версиях API.
Проблемы с encoding/json
В целом encoding/json
показал себя неплохо. Идея маршалинга и анмаршалинга произвольных типов Go в стандартное JSON-представление с возможностью его настраивать оказалась очень гибкой. Однако за годы использования пользователи выявили множество недостатков.
Недочёты существующей реализации
У существующей реализации encoding/json
есть несколько проблем:
-
Неточное обращение с синтаксисом JSON. Со временем стандартизация JSON усилилась, чтобы программы могли корректно обмениваться данными. В целом декодеры стали строже, отказываясь принимать неоднозначные входные данные, чтобы снизить вероятность того, что разные реализации по-разному интерпретируют одно и то же JSON-значение.
Сейчас
encoding/json
принимает некорректный UTF-8, хотя актуальный Интернет-стандарт (RFC 8259) требует использовать только валидный UTF-8. Поведение по умолчанию должно выдавать ошибку при наличии некорректного UTF-8, а не допускать тихую порчу данных, которая может привести к проблемам дальше по цепочке.Сейчас
encoding/json
принимает объекты с дублирующимися именами полей. RFC 8259 не регламентирует, что с ними делать, поэтому реализация может выбрать любое значение, объединить их, отбросить или выдать ошибку. Но наличие дублирующегося имени создаёт JSON-значение без однозначного смысла. Это может быть использовано злоумышленниками в приложениях, где важна безопасность, и уже использовалось ранее (например, CVE-2017-12635). Поведение по умолчанию должно быть в сторону безопасности и отклонять такие дубликаты.
Утечка nil-состояния у slice и map. JSON часто применяется для обмена с программами, где реализации JSON не допускают, чтобы
null
анмаршалился в тип данных, ожидающий массив или объект. Так какencoding/json
маршалит nil-slice или map в JSON-null, это может приводить к ошибкам при анмаршалинге другими реализациями. Опрос показал, что большинство пользователей Go предпочитают, чтобы nil-slices и maps по умолчанию маршалились как пустые массивы или объекты.Анмаршалинг без учёта регистра. При анмаршалинге имя поля объекта JSON сопоставляется с именем поля структуры Go без учёта регистра. Это неожиданное поведение по умолчанию, которое может привести к уязвимости безопасности и ограничивает производительность.
Непоследовательные вызовы методов. Из-за деталей реализации методы
MarshalJSON
, объявленные на ресивер-указателе, вызываютсяencoding/json
непоследовательно. Хотя это признано багом, исправить его нельзя — слишком много приложений уже завязаны на текущее поведение.
Недостатки API
API encoding/json
может быть неудобным или слишком ограничивающим:
Сложно правильно анмаршалить данные из
io.Reader
. Пользователи часто пишутjson.NewDecoder(r).Decode(v)
, но этот вызов не отбрасывает «мусор» в конце входного потока.Параметры можно задать у типов
Encoder
иDecoder
, но их нельзя использовать вместе с функциямиMarshal
иUnmarshal
. Аналогично, типы, реализующие интерфейсыMarshaler
иUnmarshaler
, не могут использовать параметры, и нет способа передать их по стеку вызовов. Например, параметрDecoder.DisallowUnknownFields
перестаёт действовать при вызове методаUnmarshalJSON
.Функции
Compact
,Indent
иHTMLEscape
пишут только вbytes.Buffer
вместо более гибких вариантов — например,[]byte
илиio.Writer
. Это ограничивает их применимость.
Ограничения по производительности
Помимо внутренних деталей реализации, само публичное API накладывает ряд ограничений на производительность:
MarshalJSON. Метод интерфейса
MarshalJSON
заставляет реализацию выделять память под возвращаемый[]byte
. Кроме того, по своей семантикеencoding/json
обязан проверить, что результат — корректный JSON, и дополнительно переформатировать его в соответствии с заданным отступом.UnmarshalJSON. Метод интерфейса
UnmarshalJSON
требует передачи полного JSON-значения (без лишних данных в конце). Это вынуждаетencoding/json
сначала полностью распарсить значение, чтобы определить его границы, и только после этого вызватьUnmarshalJSON
. А затем сам метод должен заново распарсить переданное JSON-значение.Отсутствие стриминга. Несмотря на то, что
Encoder
иDecoder
работают сio.Writer
иio.Reader
, они буферизуют всё JSON-значение целиком в памяти. МетодDecoder.Token
для чтения отдельных токенов создаёт много аллокаций и при этом не существует соответствующего API для записи токенов.
Кроме того, если реализация метода MarshalJSON
или UnmarshalJSON
рекурсивно вызывает функции Marshal
или Unmarshal
, производительность падает до квадратичной.
Попытка исправить encoding/json напрямую
Введение новой, несовместимой по API, мажорной версии пакета — серьёзное решение. По возможности стоит попробовать исправить существующий пакет.
Добавлять новые функции сравнительно легко, а вот менять уже существующие — сложно. К сожалению, перечисленные проблемы вытекают из текущего API, поэтому фактически их невозможно устранить, оставаясь в рамках обещания о совместимости Go 1.
Теоретически мы могли бы ввести отдельные имена вроде MarshalV2
или UnmarshalV2
, но это по сути создаст параллельное пространство имён внутри того же пакета. Отсюда и идея encoding/json/v2
(далее — v2
), где мы можем внести изменения в отдельном пространстве имён v2
в отличие от encoding/json
(далее — v1
).
Планирование encoding/json/v2
Подготовка новой мажорной версии encoding/json
заняла годы. В конце 2020 года, столкнувшись с невозможностью исправить проблемы в текущем пакете, Даниэль Марти (один из мейнтейнеров encoding/json
) впервые изложил свои мысли о том, каким мог бы быть гипотетический v2. Параллельно, после работы над Go-API для Protocol Buffers, Джо Цай был разочарован тем, что пакету protojson
пришлось использовать собственную реализацию JSON: encoding/json
не мог ни соответствовать более строгому стандарту JSON, требуемому спецификацией Protocol Buffers, ни эффективно сериализовать JSON в потоковом режиме.
Считая, что у JSON может быть более светлое будущее — и что оно достижимо, — Даниэль и Джо объединили усилия, чтобы продумать v2 и начать прототипирование (поначалу — на основе отполированной версии логики сериализации JSON из модуля Go protobuf). Со временем к работе подключились и другие (Роджер Пепп, Крис Хайнс, Йохан Брандхорст-Затцкорн и Дэмиен Нил), занимаясь обсуждением дизайна, ревью кода и регрессионным тестированием. Многие ранние обсуждения доступны публично — в записанных встречах и заметках.
Работа была открытой с самого начала, и мы всё активнее подключали сообщество Go: сначала — докладом и обсуждением на GopherCon в конце 2023 года, затем официальным предложением в начале 2025-го, и совсем недавно — принятием encoding/json/v2
в качестве эксперимента Go (доступно в Go 1.25) для широкого тестирования всеми пользователями Go.
Работа над v2 ведётся уже 5 лет, она учитывает обратную связь множества контрибьюторов и подкреплена полезным практическим опытом эксплуатации в продакшене.
Важно отметить, что в значительной степени проект разрабатывался и продвигался людьми, не работающими в Google, что демонстрирует: Go — это совместное усилие с живым мировым сообществом, нацеленным на улучшение экосистемы Go.
Опираясь на encoding/json/jsontext
Прежде чем обсуждать API v2
, познакомимся с экспериментальным пакетом encoding/json/jsontext
, который закладывает основу для будущих улучшений работы с JSON в Go.
Сериализацию JSON в Go можно разложить на два основных компонента:
синтаксические возможности, отвечающие за обработку JSON на уровне его грамматики;
семантические возможности, определяющие соответствие между значениями JSON и значениями Go.
Мы используем термины «encode» и «decode» для описания синтаксических возможностей и «marshal» и «unmarshal» — для описания семантических. Наша цель — чётко разделить функциональность, которая занимается исключительно кодированием, и функциональность маршалинга.

Эта схема иллюстрирует такое разделение. Фиолетовые блоки обозначают типы, синие — функции и методы. Направление стрелок приблизительно показывает поток данных. Нижняя половина схемы, реализованная пакетом jsontext
, содержит функциональность, работающую только с синтаксисом. Верхняя половина, реализованная пакетом json/v2
, придаёт семантический смысл синтаксическим данным, с которыми работает нижний уровень.
Базовый API jsontext
выглядит так:
package jsontext
type Encoder struct { ... }
func NewEncoder(io.Writer, ...Options) *Encoder
func (*Encoder) WriteValue(Value) error
func (*Encoder) WriteToken(Token) error
type Decoder struct { ... }
func NewDecoder(io.Reader, ...Options) *Decoder
func (*Decoder) ReadValue() (Value, error)
func (*Decoder) ReadToken() (Token, error)
type Kind byte
type Value []byte
func (Value) Kind() Kind
type Token struct { ... }
func (Token) Kind() Kind
Пакет jsontext
предоставляет средства взаимодействия с JSON на синтаксическом уровне и получил своё название по разделу 2 RFC 8259, где грамматика JSON прямо называется «JSON-text». Поскольку он работает только на уровне синтаксиса, ему не требуется рефлексия в Go.
Encoder
и Decoder
обеспечивают поддержку кодирования и декодирования JSON-значений и токенов. Их конструкторы принимают вариативные параметры, которые влияют на конкретное поведение при кодировании и декодировании. В отличие от типов Encoder
и Decoder
из v1
, новые типы в jsontext
не смешивают синтаксис и семантику и действительно работают в потоковом режиме.
JSON-значение — это завершённая единица данных, представленная в Go как именованный []byte
. Оно идентично RawMessage
из v1
. JSON-значение синтаксически состоит из одного или нескольких JSON-токенов. JSON-токен в Go представлен непрозрачным типом Token с конструкторами и методами доступа. Он аналогичен Token
в v1
, но спроектирован так, чтобы представлять произвольные JSON-токены без выделения памяти.
Чтобы решить фундаментальные проблемы производительности методов интерфейса MarshalJSON
и UnmarshalJSON
, нужен эффективный способ кодирования и декодирования JSON как потоковой последовательности токенов и значений. В v2
мы вводим методы интерфейса MarshalJSONTo
и UnmarshalJSONFrom
, которые работают с Encoder
или Decoder
, позволяя их реализациям обрабатывать JSON полностью в потоковом режиме. Таким образом, пакет json
больше не обязан проверять или форматировать JSON-значение, возвращаемое MarshalJSON
, и не должен сам определять границы JSON-значения, передаваемого в UnmarshalJSON
. Эти задачи теперь возлагаются на Encoder
и Decoder
.
Знакомство с encoding/json/v2
Опираясь на пакет jsontext
, представляем экспериментальный пакет encoding/json/v2
. Он создан для устранения упомянутых ранее проблем, при этом оставаясь привычным для пользователей v1
. Наша цель — чтобы код на v1
в основном работал так же при прямой миграции на v2
.
В этой статье мы рассмотрим в основном высокоуровневый API v2
. Примеры его использования можно найти в самом пакете v2
или в статье Антона Жиянова, посвящённом этой теме.
Базовый API v2
выглядит так:
package json
func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error
Функции Marshal
и Unmarshal
имеют сигнатуру, похожую на v1
, но дополнительно принимают параметры для настройки поведения. Функции MarshalWrite
и UnmarshalRead
работают напрямую с io.Writer
и io.Reader
, избавляя от необходимости создавать Encoder
или Decoder
только ради записи или чтения. Функции MarshalEncode
и UnmarshalDecode
работают с jsontext.Encoder
и jsontext.Decoder
и фактически представляют собой основную реализацию ранее упомянутых функций.
В отличие от v1
, параметры в v2
являются полноценным аргументом каждой функции маршалинга и анмаршалинга, что значительно расширяет гибкость и настраиваемость API. В v2 доступно множество параметров, которые не рассматриваются в этой статье.
Кастомизация на уровне типов
Как и в v1
, в v2
типы могут задавать собственное представление в JSON, реализуя определённые интерфейсы:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type MarshalerTo interface {
MarshalJSONTo(*jsontext.Encoder) error
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
type UnmarshalerFrom interface {
UnmarshalJSONFrom(*jsontext.Decoder) error
}
Интерфейсы Marshaler
и Unmarshaler
полностью идентичны интерфейсам из v1
. Новые MarshalerTo
и UnmarshalerFrom
позволяют типу представлять себя в JSON с помощью jsontext.Encoder
или jsontext.Decoder
. Это даёт возможность передавать параметры дальше по стеку вызовов, так как их можно получить через метод Options
у Encoder
или Decoder
.
См. пример OrderedObject, который показывает, как реализовать пользовательский тип, сохраняющий порядок полей объекта JSON.
Кастомизация на уровне вызова
В v2
вызывающий Marshal
и Unmarshal
также может задать собственное представление JSON для любого произвольного типа. При этом функции, определённые вызывающим, имеют приоритет над методами, реализованными самим типом, или стандартным представлением.
func WithMarshalers(*Marshalers) Options
type Marshalers struct { ... }
func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers
func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers
func WithUnmarshalers(*Unmarshalers) Options
type Unmarshalers struct { ... }
func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers
func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers
Функции MarshalFunc
и MarshalToFunc
создают кастомный маршалер, который можно передать в вызов Marshal
через WithMarshalers
, чтобы переопределить маршалинг определённых типов. Аналогично, UnmarshalFunc
и UnmarshalFromFunc
позволяют задать свою логику анмаршалинга через WithUnmarshalers
.
Пример c ProtoJSON
демонстрирует, как с помощью этой возможности вся сериализация типов proto.Message может обрабатываться пакетом protojson.
Отличия в поведении
Хотя v2
в целом стремится вести себя так же, как v1
, некоторые моменты были изменены для устранения проблем v1. Наиболее заметные изменения:
v2
выдаёт ошибку при наличии некорректного UTF-8.v2
выдаёт ошибку, если объект JSON содержит дублирующееся имя.v2
маршалит nil-slice Go как пустой массив JSON, а nil-map — как пустой объект JSON.v2
анмаршалит объект JSON в структуру Go, сопоставляя имена полей с учётом регистра.v2
переопределяет параметр тегаomitempty
: поле опускается, если оно кодируется в «пустое» JSON-значение (null
,""
,[]
,{}
).v2
выдаёт ошибку при попытке сериализоватьtime.Duration
(так как у него нет представления по умолчанию), но при этом предоставляет параметры, позволяющие вызывающему решить, как именно сериализовать.
Для большинства изменений предусмотрены теги или параметры вызова, которые позволяют настроить поведение под семантику v1
или v2
, либо выбрать другое, определённое пользователем. См. раздел «Migrating to v2».
Оптимизации производительности
Производительность Marshal
в v2
примерно сопоставима с v1
: иногда немного быстрее, иногда немного медленнее. Зато производительность Unmarshal
в v2
значительно выше — бенчмарки показывают ускорение до 10 раз.
Чтобы добиться ещё большего прироста производительности, существующим реализациям Marshaler
и Unmarshaler
стоит перейти к реализациям MarshalerTo
и UnmarshalerFrom
. Это позволит обрабатывать JSON в полностью потоковом режиме. Например, рекурсивный парсинг спецификаций OpenAPI в методах UnmarshalJSON
сильно снижал производительность в одном из сервисов Kubernetes (см. kubernetes/kube-openapi#315), тогда как переход на UnmarshalJSONFrom
улучшил её на порядки.
Более подробную информацию можно найти в репозитории go-json-experiment/jsonbench.
Улучшение encoding/json задним числом
Мы хотим избежать ситуации, когда в стандартной библиотеке Go существуют две отдельные реализации JSON, поэтому крайне важно, чтобы v1 «под капотом» была реализована через v2.
Такой подход даёт несколько преимуществ:
Постепенная миграция. Функции
Marshal
иUnmarshal
вv1
иv2
представляют собой набор стандартных поведений, работающих по семантикеv1
илиv2
. Можно указывать опции, позволяющие настроитьMarshal
илиUnmarshal
так, чтобы они работали полностью какv1
, в основном какv1
с элементамиv2
, в смешанном режиме, в основном какv2
с элементамиv1
или полностью какv2
. Это обеспечивает постепенную миграцию между версиями.Наследование возможностей. Когда в
v2
добавляются обратно совместимые функции, они автоматически становятся доступными и вv1
. Например, вv2
появились новые параметры тегов структур — такие какinline
иformat
, а также поддержка интерфейсовMarshalJSONTo
иUnmarshalJSONFrom
, которые более производительны и гибки. Еслиv1
реализована черезv2
, она наследует и эти возможности.Снижение затрат на сопровождение. Поддержка широко используемого пакета требует значительных усилий. Если
v1
иv2
используют одну реализацию, нагрузка снижается: одно изменение исправляет баги, улучшает производительность или добавляет функциональность сразу для обеих версий. Нет необходимости отдельно переносить изменения изv2
вv1
.
Хотя отдельные части v1
со временем могут быть признаны устаревшими (если v2
выйдет из статуса эксперимента), весь пакет никогда не будет объявлен устаревшим. Переход на v2
будет поощряться, но не станет обязательным. Проект Go не откажется от поддержки v1
.
Эксперименты с jsonv2
Новые API в пакетах encoding/json/jsontext
и encoding/json/v2
по умолчанию не видны. Чтобы использовать их, соберите код с установленной переменной окружения GOEXPERIMENT=jsonv2
или с билд-тегом goexperiment.jsonv2
. Суть эксперимента в том, что API нестабилен и может измениться в будущем. Тем не менее реализация обладает высоким качеством и уже успешно применяется в продакшене несколькими крупными проектами.
Поскольку v1
реализована через v2
, при сборке с экспериментальным jsonv2
внутренняя реализация v1
полностью меняется. Без изменений в коде вы можете прогнать свои тесты под jsonv2
— теоретически не должно появиться новых сбоев:
GOEXPERIMENT=jsonv2 go test ./...
Переходная реализация v1
через v2
стремится к идентичному поведению в рамках обещания совместимости Go 1, хотя могут встречаться различия, например, в формулировке сообщений об ошибках. Мы призываем вас запускать тесты под jsonv2
и сообщать о любых регрессиях в ишью трекере.
Появление jsonv2
в качестве эксперимента в Go 1.25 — важный шаг на пути к официальному включению encoding/json/jsontext
и encoding/json/v2
в стандартную библиотеку. Однако цель эксперимента — собрать более широкий опыт. Именно ваша обратная связь определит дальнейшие шаги и итог этого эксперимента, который может завершиться как отказом от идеи, так и принятием пакетов в стабильном виде в Go 1.26. Поделитесь своим опытом на go.dev/issue/71497 и помогите определить будущее Go.
Русскоязычное Go сообщество

Друзья! Эту статью перевела команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!