Полиморфная сериализация 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"}
Ссылки
ссылка на оригинал статьи https://habr.com/ru/articles/927732/
Добавить комментарий