Search
Write a publication
Pull to refresh

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

Level of difficultyEasy
Reading time3 min
Views2.9K
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"}

Ссылки

Tags:
Hubs:
+3
Comments7

Articles