В процессе работы над своим дипломом я столкнулся с интересной задачей: сформировать контент нескольких типов в один JSON-массив. Поскольку бэкенд написан на C#, то и пример будет для него. Например есть контент-ссылка (есть идентификатор и URL), контент-текст (есть идентификатор и содержимое) и контент-файл (есть идентификатор и название). Нужно как-то эти очевидно разные типы данных засунуть в одну кучу, чтобы приложение могло удобно это обрабатывать.
Первое, что приходит на ум, создать один жирный объект с nullable-полями, в который засовывать разные типы. Навроде такого:
Что на выходе нам даст примерно это:
Даже с маленьким количеством полей выглядит паршиво, не правда ли? А что будет, если необходимые клиенту данные разростутся? Так что этот вариант я не стал даже рассматривать.
Попытка номер раз
Первой моей идеей было создать интерфейс с общими полями, от которого бы наследовались разные типы контента. Поскольку List<T> в C# не поддерживает полиморфизм, пришлось использовать IEnumerable<T>. Так получилась такая модель данных:
И хотя при создании коллекции из элементов контента проблем с полиморфизмом не было, сериализатор выдавал объекты, приведённые к IContentItem:
Такое поведение (естественно) меня не устраивало. В Newtonsoft.Json разрешить полиморфизм при сериализации можно одним параметром. Но я во всём приложении использую System.Text.Json, поэтому и говорить буду про него. Здесь такого параметра нет, значит нужно написать свой конвертер для типа IContentItem, который поможет правильно сериализовать объект в зависимости от типа контента.
Попытка номер два
Добавим куда-нибудь в проект кастомный конвертер ContentConverter. Он должен реализовать интерфейс JsonConverter<T>. В качестве параметра типа укажем интерфейс:
По сути тут происходит несложная логика. На вход конвертеру поступает объект, в котором записано JSON-дерево. Оттуда его можно эффективно считать и обработать. Это мы и делаем — достаём из объекта свойство Type и на его основе решаем, какой десериализатор использовать.
Останется только подключить этот конвертер, чтобы парсер начал его использовать. Поскольку я использую FastEndpoints, у меня это делается просто:
Теперь метод будет возвращать красивый JSON с объектами разного типа:
Окей, а как это парсить на клиенте?
Хороший вопрос, и ответ на него несложный. kotlinx.serialization умеет в полиморфную сериализацию. Достаточно написать кастомный сериализатор, который подскажет, какие типы в зависимости от чего брать. Для начала добавим классы, которые опишут модели данных, которые приходят из API.
Опишем вот такой базовый тип, от которого будут наследоваться все типы контента:
И классы, описывающие конкретные типы контента:
Эти типы различаются не только набором полей, но и типом контента. Он описан перечислением:
Для того, чтобы он корректно парсился из JSON, добавим ему кастомный сериализатор:
Теперь понадобится сериализатор для базового типа контента, который будет понимать, по какому принципу выбирать сериализатор для конкретных типов:
Теперь перед тем, как пытаться привести весь объект контента к базовому типу, сериализатор посмотрит на поле contentType и если найдёт там знакомое значение, будет использовать корректный сериализатор.
Добавим вот такой сериализуемый класс, который содержит в себе список базовых элементов контента
Отлично, теперь можно работать с запросами как обычно:
Заключение
В этой статье я показал, как можно добавить отправку массивов JSON с объектами разных типов из бэкенда на C#/.NET и парсинг этих данных обратно в объекты в приложении на kotlin.