JsonPolymorphismDemo
Минимальный пример на C# (.NET 9): полиморфизм + JSON через System.Text.Json.
Одна точка приёма строки — один сериализатор и один десериализатор на всё приложение.
Клиент не задумывается, какую строку он передаёт, — но получает валидный объект.
Дерево классов
Message (abstract)
├── GameMessage (abstract) — принимает Game
│ ├── Data
│ ├── Command
│ ├── SetSettings
│ └── InitChannels
└── KinesisMessage (abstract) — принимает Kinesis
└── RequestLimitOffset
Два промежуточных абстрактных класса задают направление движения JSON (Game / Kinesis),
у каждого — свои потомки. Клиент кода работает только с корнем Message.
Формат JSON
Тип сообщения различается полем-дискриминатором $type (имя класса):
{"$type":"Command","action":"move","args":["north","3"]}
{"$type":"RequestLimitOffset","limit":50,"offset":100}
Сериализация и десериализация дают один и тот же формат (round-trip).
Единственная точка приёма
Message msg = MessageJson.Deserialize(json); // любая строка -> валидный Message
string json = MessageJson.Serialize(msg); // Message -> та же строка
Внутри — один JsonSerializerOptions и один JsonConverter<Message>
(MessageJson.cs). Вся настройка локальна, глобальное состояние не трогается.
Направление выбирается обычным pattern matching по абстрактным уровням:
string route = msg switch
{
GameMessage => "Game",
KinesisMessage => "Kinesis",
_ => "???",
};
Ошибка на неизвестном/отсутствующем типе — JsonException, без «молчаливых»
значений по умолчанию.
Пути полиморфизма в JSON и выбор
| Путь | Как работает | Плюсы | Минусы |
|---|---|---|---|
System.Text.Json JsonPolymorphic/JsonDerivedType-атрибуты (A) |
атрибуты на корне перечисляют потомков | встроено в STJ, минимум кода | список потомков размазан атрибутами по классу; иерархия Game/Kinesis остаётся скрытой; добавление класса = правка атрибутов |
Свой конвертер JsonConverter<Message> (B) |
вручную читаем/пишем $type, дальше обычный STJ |
полный контроль формата; явная таблица «имя → тип» в одном месте; различия корректно ловятся | ~50 строк кода конвертера |
Newtonsoft TypeNameHandling (C) |
сериализатор сам вставляет $type с именем типа |
ноль своего кода | небезопасно при приёме чужих строк: имя типа разрешается в любой тип сборки (уязвимость из документации Newtonsoft); отдельная зависимость |
Выбран путь B (свой конвертер): формат задан явно (короткий $type),
таблица типов собрана в одном месте (MessageJson.ByType), произвольных
TypeNameHandling-подстановок нет — вход доверяет только пяти известным именам.
Как расширить
Новый тип сообщения = два шага, без правки конвертера:
- объявить класс-потомок
GameMessageилиKinesisMessage; - добавить одну строку в
MessageJson.ByType.
Структура
Messages/Messages.cs— дерево классов (только данные, без атрибутов);MessageJson.cs— единственный сериализатор/десериализатор;Program.cs— пример использования: приём строк, round-trip, обработка ошибки, маршрутизация по направлению.
Запуск
dotnet run
Пример вывода:
{"$type":"Data","payload":"hello"} -> Data
...
JsonException: Unknown message type: "Nope".
Data -> Game
RequestLimitOffset -> Kinesis