Полиморфизм в JSON в Go

от автора

PolyJSON

PolyJSON

Полиморфная сериализация 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 — обёртка с использованием дженериков;

  • polygen — генератор кода, расширяющий возможности poly.

Библиотека 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *