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"}

Ссылки

Комментарии (3)


  1. DimNS
    29.07.2025 06:37

    Я считаю что явное лучше неявного

    type ItemText struct {
      Content string `json:"content"`
    }
    
    type ItemImage struct {
      URL string `json:"url"`
    }
    
    type Items struct {
      Texts  []ItemText  `json:"texts,omitempty"`
      Images []ItemImage `json:"images,omitempty"`
    }


    1. ykalchevskiy Автор
      29.07.2025 06:37

      Явное, конечно, лучше, но это уже совсем другая структура. Возможно, нужно было использовать другой пример, что-то такое:

      type IsAction interface {
      	IsAction()
      }
      
      type ActionAlert struct{
      	Text string `json:"text"`
      }
      
      func (ActionAlert) IsAction() {}
      
      type ActionDeepLink struct {
      	URL string `json:"url"`
      }
      
      func (ActionDeepLink) IsAction() {}
      
      type Button struct {
      	Text string
      	Type string
      	Action Action // HERE
      }

      Button является уже зафиксированым контрактом и нужно работать с JSON вида:

      {
      	"text": "Click me",
      	"type": "primary",
      	"action": {
      		"text": "It's an alert!"
      	}
      }


  1. Gorthauer87
    29.07.2025 06:37

    Вот почему они в Go не добавили алгебраические типы данных? Это же блин как без одной руки жить. Можно но неудобно.