
Полиморфная сериализация JSON — частая задача при проектировании API, UI-моделей или событийных структур. Пример структуры:
[
{"type": "text", "content": "hello"},
{"type": "image", "url": "pic.jpg"}
]
В Go такие данные принято представлять с помощью интерфейсов. Однако стандартный пакет encoding/json
не умеет автоматически сериализовать и десериализовать структуры с полем-дискриминатором (например, "type"
), которое определяет конкретный подтип. Приходится либо использовать громоздкие конструкции вроде map[string]any
или json.RawMessage
, либо вручную реализовывать интерфейсы json.Marshaler
и json.Unmarshaler
с разбором каждого варианта — такой подход быстро становится неудобным и слабо масштабируется.
Для решения этой задачи были разработаны две библиотеки:
Библиотека poly
poly
реализует сериализацию и десериализацию JSON на основе интерфейсов и дженериков. Подтипы регистрируются через poly.TypesN[...]
и реализуют интерфейс poly.TypeName
с методом TypeName() string
, определяющим значение поля "type"
.
Объявление типов
type Item = poly.Poly[IsItem, poly.Types2[TextItem, ImageItem]]
type IsItem interface {
poly.TypeName // необязательно явно, но удобно
isItem()
}
type TextItem struct {
Content string `json:"content"`
}
func (TextItem) isItem() {}
func (TextItem) TypeName() string { return "text" }
type ImageItem struct {
URL string `json:"url"`
}
func (ImageItem) isItem() {}
func (ImageItem) TypeName() string { return "image" }
Десериализация
var item Item
_ = json.Unmarshal([]byte(`{"type":"text","content":"hello"}`), &item)
// item.Value => TextItem{Content: "hello"}
_ = json.Unmarshal([]byte(`{"type":"image","url":"pic.jpg"}`), &item)
// item.Value => ImageItem{URL: "pic.jpg"}
_ = json.Unmarshal([]byte(`{"url":"new.jpg"}`), &item)
// item.Value => ImageItem{URL: "new.jpg"}
Сериализация
item = Item{Value: TextItem{Content: "Hi"}}
data, _ := json.Marshal(item)
// {"type":"text","content":"Hi"}
item = Item{Value: ImageItem{URL: "pic.jpg"}}
data, _ = json.Marshal(item)
// {"type":"image","url":"pic.jpg"}
Зачем появился polygen
poly
задумывался как лёгкое решение без генерации кода. Однако по мере развития стало ясно, что многие необходимые возможности не удаётся реализовать без усложнения API:
настройка имени поля-дискриминатора;
строгий режим (ошибка при неизвестном поле (DisallowUnknownFields));
дефолтное поведение при отсутствии
"type"
;масштабируемость при большом числе вариантов.
Чтобы не перегружать poly
, был создан отдельный инструмент — polygen
, который решает эти задачи через генерацию кода на основе файла конфигурации.
Объявление типов
type IsItem interface {
isItem()
}
type TextItem struct {
Content string `json:"content"`
}
func (TextItem) isItem() {}
type ImageItem struct {
URL string `json:"url"`
}
func (ImageItem) isItem() {}
Конфигурация .polygen.json
{
"$schema": "https://raw.githubusercontent.com/ykalchevskiy/polygen/refs/heads/main/schema.json",
"types": [
{
"type": "Item",
"interface": "IsItem",
"package": "main",
"subtypes": {
"TextItem": {
"name": "text"
},
"ImageItem": {
"name": "image"
}
}
}
]
}
Описание этих и остальных параметров можно посмотреть в README и в документации.
Генерация
$ go install github.com/ykalchevskiy/polygen@latest
$ polygen
Генерируется файл item_polygen.go
с типом Item
, реализующим сериализацию/десериализацию по полю "type"
.
Десериализация
var item Item
_ = json.Unmarshal([]byte(`{"type": "text", "content": "hello"}`), &item)
// item.IsItem => TextItem{Content: "hello"}
_ = json.Unmarshal([]byte(`{"type": "image", "url": "pic.jpg"}`), &item)
// item.IsItem => ImageItem{URL: "pic.jpg"}
_ = json.Unmarshal([]byte(`{"url": "new.jpg"}`), &item)
// item.IsItem => ImageItem{URL: "new.jpg"}
Сериализация
item = Item{IsItem: TextItem{Content: "Hi"}}
data, _ := json.Marshal(item)
// {"type":"text","content":"Hi"}
item = Item{IsItem: ImageItem{URL: "pic.jpg"}}
data, _ = json.Marshal(item)
// {"type":"image","url":"pic.jpg"}
Ссылки
Комментарии (3)
Gorthauer87
29.07.2025 06:37Вот почему они в Go не добавили алгебраические типы данных? Это же блин как без одной руки жить. Можно но неудобно.
DimNS
Я считаю что явное лучше неявного
ykalchevskiy Автор
Явное, конечно, лучше, но это уже совсем другая структура. Возможно, нужно было использовать другой пример, что-то такое:
Button является уже зафиксированым контрактом и нужно работать с JSON вида: