
Изменения интерфейса мобильного приложения часто упираются не в сложность реализации, а в скорость релизного цикла: даже простые правки проходят через полный конвейер — разработку, рецензирование, сборку и публикацию. При высокой частоте изменений это увеличивает time-to-market, перегружает команду и делает быстрые итерации по интерфейсу практически невозможными.
Меня зовут Михаил Рыбочкин, я бэкенд-разработчик в компании GRI. Участвую в разработке и поддержке платформы для крупного ювелирного ритейлера. Я расскажу, как реализован Server-Driven UI для электронной коммерции с более чем 1000 розничных магазинов; как устроено управление конфигурацией интерфейса через Django Admin и как это позволяет менять интерфейс без релизов приложения; какие у этого подхода есть ограничения и какой инцидент произошёл в эксплуатации. Особенность нашего подхода в том, что SDUI одновременно обслуживает и нативные мобильные приложения, и веб на Vue. Один конфиг, один API, две целевых платформы
Django-монолит, 99 приложений и высокая частота изменений
Сначала опишу контекст проекта, чтобы было понятно, в каких условиях мы принимали решения.
Крупная e-commerce-платформа для ювелирной розницы. Бэкенд сделан на Django-монолите (99 Django-приложений). Мобильное приложение под iOS и Android, веб на Vue 3.
Стек: Django 5, Python 3.12, PostgreSQL, ClickHouse, Valkey, Celery + RabbitMQ, Kafka, Vue 3 + TypeScript. Платформа обслуживает более 1000 розничных магазинов по России, предоставляет REST API для мобильных приложений и интегрируется с системой 1С, платёжными системами, службами доставки и так далее, все не счесть.
Релизный цикл мобильного приложения — раз в две недели. При этом интерфейс и содержимое главной страницы требуют более частых изменений, включая промо-активности и сезонные кампании, которые готовятся загодя. Каждое такое изменение требует ресурсы фронтенда, проведение PR, рецензирования и релиза, а также нужно проходить процедуру публикации в магазинах приложений.
При этом продукт уже находится в эксплуатации, и «переписать всё» с нуля нельзя. Решение должно жить внутри существующего монолита и работать со старыми версиями приложения, которые клиенты могут не обновлять годами.

Почему не WebView?
Когда встаёт вопрос о гибкой настройке интерфейса, первым вспоминают об использовании WebView. UX ювелирной розницы требует нативных компонентов: плавного масштабирования фотографий, анимации каруселей, жестов в stories. В WebView всё это работает заметно хуже. Также есть сложности с публикацией таких приложений в AppStore, PlayMarket и HuaweiStore.
Для нас SDUI — это способ управления тем, что действительно часто меняется, а то, что требует нативной производительности и интеграций, остаётся в клиентском коде.
Почему не взяли готовое решение?
Подход Server-Driven UI (он же Backend-Driven UI) не является новым. Его используют различные международные и российские корпорации, банки. Мы рассматривали эти решения, но нам ни одно не подошло.
DivKit — зрелый движок, декларативно описывающий интерфейс через JSON, вплоть до отступов и шрифтов. По сути, это низкоуровневый движок макетов: JSON определяет не только «что показать», но и «как это свёрстано». Для нас это избыточно. У нас есть дизайн-система и готовые компоненты как на Vue, так и на мобилках. Задача не в том, чтобы описывать вёрстку через JSON, а в том, чтобы управлять конфигурацией уже существующих компонентов. Переход на DivKit означал бы переписывание компонентов в формат движка — слишком большая работа ради гибкости, которая нам не нужна.
Airbnb Ghost Platform — интересный подход, но там выделенная инфраструктурная команда, собственный протокол, свой SDK для рендеринга. У нас нет ресурсов, чтобы делать фреймворк, нам нужно поставлять фичи.
Headless CMS решают задачу управления контентом и его структурой. Но нам эти системы всё равно не подошли, так как у нас много специфики. Например, блоки главной страницы у нас зависят от региона, диапазона версий приложения, дат активности и принадлежности к A/Б-тесту, и эту фильтрацию выполняет бэкенд при сборке ответа. В Strapi или Directus пришлось бы описывать эти сущности через их механизм контент-типов и писать логику фильтрации снаружи — фактически, делать надстройку над CMS, которая реализует нашу доменную логику.
Мы выбрали другой путь: пошаговое наращивание SDUI внутри существующей платформы, по мере появления задач.
SDUI в других отраслях

В России SDUI активно применяют в банковской сфере. Некоторые крупные банки создают низкоуровневые SDUI поверх своей дизайн-системы: JSON задаёт не только данные, но и layout — расположение и ограничения элементов, вплоть до отдельных полей. Это даёт максимальную гибкость: бэкенд управляет интерфейсом на уровне отдельных виджетов, а не только их содержимым. Это оправдано в случаях, когда нужно собирать сложные формы оплаты или экраны продуктов без переиздания мобильного приложения.
В крупных маркетплейсах встречается ещё более масштабный вариант: тысячи виджетов, десятки тысяч шаблонов страниц, отдельные внутренние системы управления макетами и сборки ответов. Фактически, это полноценная платформа, над которой работают десятки команд.
Мы ориентировались на опыт крупных маркетплейсов, но не на их масштаб — у нас задача и ресурсы другие.
Как SDUI появился в проекте

С самого начала SDUI не проектировали как отдельную систему. Он рос органически: задача за задачей.
В 2021 году этот подход в первую очередь использовали для работы с баннерами на главной странице: одиночные баннеры, карусели, кнопки-ссылки. Для управления порядком и видимостью блоков появилась модель MainPageBlock, доступная через Django Admin. Каждый тип имел свой сериализатор на бэкенде и соответствующий компонент на фронтенде. По сути, это был конфигурируемый CMS-слой: «покажи эти блоки в таком порядке». Цвета и шрифты оставались на стороне фронтенда.
По мере роста задач требования начали усложняться. В 2022-м появились новые динамические блоки: рекомендации, персональные подборки, коллекции. Их тоже необходимо было размещать на главной странице и управлять их порядком, однако возможностей MainPageBlock уже не хватало. Появились блоки, предоставляющие фронтенду дополнительную визуальную информацию.
В 2022-м внедрили JsonConfigEntry — универсальный механизм для JSON-конфигураций в БД. Изначально его использовали для настройки не столько интерфейса, сколько разных технических параметров: фича-флагов, платёжных методов, параметров служб доставки и других сущностей, влияющих на поведение бэкенда без развёртывания. Тогда же стали появляться первые кастомные валидаторы для конфигураций.
С 2023-го по 2024-й продолжилось развитие конфигураций и оркестратора блоков. Через API начали передавать визуальную конфигурацию: HEX-коды цветов, URL фоновых картинок, параметры градиентов. Фронтенд стал соотносить их с CSS-переменными и рендерить. Интерфейс стал гибче, и редактирование JSON-конфигураций всё чаще ложилось на маркетинг заказчика.
Всегда ли у них получалось делать это правильно?

В 2025-м мы решили эту проблему, реализовав PydanticSchemaRenderService — сервис, который по Pydantic-схеме генерирует HTML-форму в Django Admin. Color picker вместо ввода HEX-кода, чекбоксы вместо true/false, выпадающие списки для enum. На нашей стороне была техническая реализация SDUI в проекте заказчика и запуск новых фич, управление которыми полностью отдаётся в админку для маркетологов и контент-команды заказчика.
Архитектура SDUI
SDUI у нас держится на трёх столпах, каждый из которых отвечает за свою задачу. При этом конкретные фичи могут использовать один, два или все три слоя одновременно — в зависимости от требований.

Столп №1 — оркестратор: какие блоки показать и в каком порядке
Главная страница представляет собой динамически собираемый на бэкенде массив блоков:
class MainPageBlock(models.Model): class TypeChoices(models.TextChoices): CARD_SLIDER = 'card_slider' CIRCLE_STORIES = 'circle_stories' PRODUCT_SHELF = 'product_shelf' ACTION_SLIDER = 'action_slider' CERTIFICATE_SLIDER = 'certificate_slider' DYNAMIC_BANNER = 'dynamic_banner' # ... ещё 17 типов type = models.CharField(choices=TypeChoices.choices) is_published = models.BooleanField() version = models.IntegerField() # мин. версия приложения until_version = models.IntegerField() # макс. версия regions = models.ManyToManyField(Region) # гео-таргетинг start_date = models.DateTimeField() end_date = models.DateTimeField()
Бэкенд выбирает сериализатор через маппинг по типу:
def serialize(self, region_id, block_index, ...): serializers_mapping = { self.TypeChoices.CARD_SLIDER: MainPageCardSliderSerializer, self.TypeChoices.CIRCLE_STORIES: MainPageCircleStorySerializer, self.TypeChoices.PRODUCT_SHELF: MainPageVerticalShelfSerializer, self.TypeChoices.ACTION_SLIDER: MainPageActionSliderSerializer, # ... 23+ типов } serializer = serializers_mapping[self.type] return serializer(self, context={...}).data
На фронтенде используется зеркальный реестр
const componentByType = { card_slider: CardSlider, circle_stories: CircleStories, product_shelf: ProductShelf, action_slider: ActionSlider, // ... }

API отдаёт массив блоков, предварительно отфильтрованный по региону, версии приложения, датам, A/Б-тестам. Фронтенд итерирует и рендерит блоки на основе их типа. Никаких if-ов «показать или нет» — вся логика на стороне бэкенда.
Столп №2 — визуальная конфигурация: как блоки выглядят
MainPageBlock работает как оркестратор. Метаинформацию и картинки он хранить умеет, но для новых фич этого не хватало, так как блоки на главной странице становились всё разнообразнее, а бесконечно расширять модель MainPageBlock нельзя. Под каждую фичу появились свои специализированные модели, отвечающие за визуальную спецификацию. При этом фронтенд не решает, каким цветом красить заголовок, а сразу получает готовый HEX.
Рассмотрим на примере продуктовой полки. Бэкенд отдаёт всю информацию:
{ "blocks": [ { "id": 393, "type": "circle_stories" }, { "id": 391, "type": "product_shelf", "shelf_data": { "id": 6, "name": "Коллекции", "name_color": "#080901", "tabs": [ { "id": 19, "header": "СВАДЬБА И ПОМОЛВКА", "header_color": "#f3f2f2", "header_color_inverted": "#000000", "selected_collection_rect_color": "#000000", "image": { "url": "", "width": 390, "height": 516 }, "url": "/catalog/bracelets.html", "view": "catalog", "view_data": "", "button_label": "Смотреть все", "analytics": { // тут наши данные для аналитики } } ] } }, … ] }
На стороне фронтенда данные переводятся в CSS-переменные и блоки:
const styleVars = computed(() => ({ '--shelf-name-color': shelfData.value.name_color, '--shelf-tab-header-color': activeTabData.value.header_color, '--shelf-tab-rect-color': activeTabData.value.selected_collection_rect_color, }));
.shelf-title { color: var(--shelf-name-color); } .tab-header { color: var(--shelf-tab-header-color); } .tab-selected { background: var(--shelf-tab-rect-color); }
Визуальные параметры меняются через Django Admin. При этом изменения сразу применяются в приложении без PR и развёртывания.
Навигация также определяется на бэкенде. Поля view и view_data описывают, куда ведёт клик из более чем 45 экранов приложения.
Столп №3: конфигурирование без развёртывания
Некоторые блоки управляются не через Django-модели, а через JSON-конфигурации в базе. Менять их можно без развёртывания, прямо в Django Admin. Фундамент этого механизма — JsonConfigEntry.
Django-модель, хранящая произвольный JSON:
class JsonConfigEntry(models.Model): slug = models.SlugField(unique=True) name = models.CharField(max_length=255) data = models.JSONField() @classmethod def get_data_by_slug(cls, slug, *, safe=False, cache_duration=None): # Redis-кэш ...
Каждая конфигурация привязана к Pydantic-модели. Сохраняешь в Django Admin — Pydantic проверяет структуру. Не прошло проверку — не сохранится, в админку выводятся понятные человеку сообщения.
Проверка обеспечивает целостность данных, но не решает задачу удобства: маркетолог заказчика по-прежнему работает с сырым JSON, что может стать для него проблемой.

Генератор форм по Pydantic-схеме
Проблему сложности работы с сырым JSON решает PydanticSchemaRenderService. Он берёт Pydantic-схему конфигурации и генерирует HTML-форму.
Сервис принимает Pydantic-модель и рекурсивно обходит её поля. Для каждого поля смотрит аннотацию типа и выбирает HTML-виджет
if annotation is bool: return RenderFieldBool(...) # чекбокс elif annotation is ColorFieldType: return RenderFieldColor(...) # color picker elif annotation is str: return RenderFieldText(...) # текстовое поле elif annotation in (int, float): return RenderFieldInteger(...) # числовое поле # datetime, enum, вложенные модели...
Одна и та же конфигурация поддерживает два способа редактирования: сырой JSON для разработчиков и сгенерированную форму для пользователей админки. Pydantic-схема при этом выступает единым источником истины для проверки и генерирования интерфейса.
По сути, это своего рода SDUI для самого SDUI: бэкенд управляет не только интерфейсом приложения, но и интерфейсом своей админки.
Подход базовый: одно поле схемы — один виджет, сложных редакторов (например, drag-and-drop для порядка блоков, визуального предпросмотра) пока нет. Для нашей задачи замены сырого JSON на форму для маркетинга этого достаточно.
JSON-конфигурация в админке выглядит так:

После добавления абстрактного класса к его Django форме:
class EntryExtensionForm(EntryExtensionFormBase): validation_model = TickerConfig
он приобретает вид и функциональность обычной Django формы:

Под капотом механизм устроен проще, чем может показаться. Начнём с точки входа: сервис принимает Pydantic-модель (схему конфигурации), сами данные и опциональные ошибки проверки для их отображения в форме:
class PydanticSchemaRenderService: def init( self, validation_model: type[BaseModel], data: BaseModel | dict[str, Any], errors: dict | None = None, context: dict | None = None, ): …
Дальше — главная функция рекурсивного обхода полей. Для каждого поля Pydantic-модели извлекается текущее значение (или значение по умолчанию, если данных нет), а также аннотация типа, включая параметры generic-типов (например, list[X] или Optional[Y]):
def renderpydantic_schema_recursive( self, validation_model: type[BaseModel], data: BaseModel | dict[str, Any] | None = None, call_stack: list[str] = None, ): html = [] new_items_mapper = {} if not call_stack: call_stack = ['jsonconf'] for name, field in validation_model.model_fields.items(): # Достаём текущее значение поля из данных if data is not None: value = getattr(data, name) if issubclass(data.__class__, BaseModel) else data.get(name) elif field.default is not None: value = field.default else: value = None if isinstance(value, PydanticUndefinedType): value = None # Разбираем тип поля annotation = field.annotation origin = get_origin(annotation) args = get_args(annotation)
Основная логика заключается в диспетчеризации по типу поля. На практике выделяют три ветки, которые покрывают 99% реальных случаев: простые типы (скаляры, цвета, даты, enum), списки и вложенные Pydantic-модели.
# Простое поле: bool, str, int, ColorFieldType, Enum, datetime if rendered_simple_field := self._try_render_simple_field( name=name, field=field, value=value, annotation=annotation, origin=origin, args=args, call_stack=call_stack, ): html.append(rendered_simple_field) # Список — отдельный рендерер с кнопками «Добавить» / «Удалить» elif origin is list and args: item_type = args[0] html, new_items_mapper = self._render_list( name=name, field=field, value=value, annotation=item_type, call_stack=call_stack, ) html.append(_html) new_items_mapper.update(_new_items_mapper) # Вложенная Pydantic-модель — рендерим как подсекцию формы elif args and any(issubclass(arg, BaseModel) for arg in args): item_type = args[0] html, new_items_mapper = self._render_sub_model( name=name, value=value, field=field, annotation=item_type, call_stack=call_stack, ) html.append(_html) new_items_mapper.update(_new_items_mapper) return ''.join(html), new_items_mapper
Первая ветка делегирует рендер в _try_render_simple_field. В этой части живёт проекция конкретного типа на HTML-виджет (bool — чекбокс, ColorFieldType — color picker, Enum — <select>, и так далее).
Вторая и третья ветки — рекурсивные: список или вложенная модель становятся отдельными подсекциями формы, каждая со своим набором полей.
Новые типы полей добавляют расширением _try_render_simple_field, при этом структура рекурсии не меняется. За счёт этого сервис расширяется под любую новую предметную область без переписывания ядра.
Примеры использования
Ниже представлены примеры фич, демонстрирующие применение SDUI на практике.
Горизонтальные полки
Заказчик захотел тематические подборки товаров на главной странице, с вкладками и настраиваемыми цветами, которые можно создавать и редактировать через админку.
class Shelf(TimeWatchingMixin, SortableMixin, IsPublishedMixin): """Горизонтальная полка товаров""" name = models.CharField(max_length=100, blank=True) name_color = models.CharField(max_length=7) # HEX class ShelfProductList(TimeWatchingMixin, SortableMixin, AnalyticsBannerMixin): """Таб внутри полки""" shelf = models.ForeignKey(Shelf, on_delete=models.CASCADE) collection = models.ForeignKey(ProductCollection, ...) header = models.CharField(max_length=30) header_color = models.CharField(max_length=7) header_color_inverted = models.CharField(max_length=7) selected_collection_rect_color = models.CharField(...) image = models.ImageField(...) view = models.CharField(choices=AppViews.choices) # deeplink view_data = models.CharField(...)
Содержимое вкладок загружается по востребованию, с использованием IntersectionObserver на фронтенде. Если полка попала во viewport, то для неё запрашиваются товары. Данные кешируются с TTL за пять минут. Без этого основной ответ главной страницы с более чем 20 блоками тащил бы за собой огромный объём товарных карточек.
Как это работает на практике. Например, создают в Django Admin полку «Подарки к 8 марта», в которой:
через color picker задают цвета;
привязывают коллекции (например, «Серьги», «Кольца», «Подвески»);
указываются даты активности;
создают MainPageBlock на нужной позиции.
После сохранения конфигурации изменения становятся доступны в течение нескольких минут без участия разработчиков.

Circle Stories — градиенты и deeplinks

Stories — формат видео, адаптированный для ювелирной розницы. Он представлен в виде круглых превью с градиентными рамками и слайдов с картинками, видео и кнопками.
Что контролирует бэкенд:
Градиенты рамок. Параметры
border_color_from,border_color_toиborder_weightопределяют внешний вид рамки видео. Например, для сезонных кампаний используют различные цветовые комбинации.Deeplinks. Переходы задают через поля
btn_viewиbtn_view_data. Каждую кнопку привязывают к одному из более чем 45 экранов приложения. Фронтенд не знает, что «это кнопка для промоакции», он просто обрабатывает паруview/view_data.Тайминг. Параметр
image_timerопределяет длительность показа каждого слайда. Значение задают на стороне бэкенда.Кеширование. Мы используем MD5-хеш контента. TTL составляет пять минут, однако при изменении контента хеш обновляется и клиент получает свежие данные.
Бегущая строка
Бегущая строка внизу экрана называется «тикер». Её используют для промо-сообщений, анонсов, уведомлений. В отличие от предыдущих примеров, тикер не использует MainPageBlock и полностью реализован на основе JsonConfigEntry. Та же инфраструктура, но с другой точкой входа.
Выглядит бегущая строка примерно так:

Но при желании может и так:

Отдельного внимания заслуживает развитие инструментов работы с конфигурациями. Для этой фичи мы даже внедрили на платформе принципиально новую функциональность — генератор форм Django Admin по Pydantic-схеме валидатора конфигурации PydanticSchemaRenderService.
Однажды мы столкнулись с проблемой: маркетинг и контент-команда заказчика могли заполнять простые JSON-конфигурации, чтобы обновить надписи в интерфейсе или добавить картинки, но схема тикера оказалась достаточно сложной, и риск ошибки при ручном редактировании JSON стал высоким. Ранее для защиты от этого добавили проверку через Pydantic, которая не давала сохранить некорректные данные. На этот раз мы пошли дальше и создали инструмент для визуального редактирования конфигураций.
За март 2025 года мы внесли в тикер 11 изменений. Каждое — через Django Admin, без коммитов и без отвлечения команды разработки от создания новых фич.
Проблемы подхода
SDUI решает свою задачу, но не бесплатно. За время развития платформы мы столкнулись с несколькими повторяющимися классами проблем:
1. Раздувание ответа и ленивые загрузки. Главная страница с более чем 20 блоками формирует значительный по объёму JSON. Если каждый блок будет тащить за собой все связанные данные (товары, медиа, аналитику), то основной ответ распухает до сотен килобайтов ещё до того, как пользователь увидит первый экран. При медленном мобильном интернете это критично.
Мы решили загружать данные «лениво». Основной ответ содержит только каркас страницы: список блоков, их типы, визуальные параметры и метаданные. Тяжёлые данные подгружаются отдельными запросами по мере необходимости. Полки запрашивают товары через IntersectionObserver, когда попадают во viewport.
Тикер обновляется по своему эндпоинту с собственным TTL. Слайдеры (например, бонусов и сертификатов) загружаются при первом взаимодействии или пролистывании.
Это усложняет схему API: вместо одного запроса главной получается дерево из 5—10 запросов, но даёт лучший UX и позволяет независимо кешировать разные части страницы.
2. Скорость изменения требований.Темп изменений со стороны заказчика выше, чем цикл платформенных изменений. Это не недостаток SDUI, а свойство процесса, к которому нужно быть готовым.
Добавление новых возможностей инструмента приводит к появлению новых требований. Часть таких запросов закрывают в рамках существующей архитектуры, часть требует нового кода на бэкенде и фронте. Чем удобнее инструмент, тем больше идей он порождает. Мы делаем полки с настраиваемыми цветами — маркетинг хочет анимированные фоны. Добавляем фоновые картинки — хотят видео. Делаем видео — хотят интерактивные элементы.
SDUI не убирает разработку — он сокращает цикл для типовых задач. Если большая часть маркетинговых запросов закрывается через Django Admin, это уже значимый выигрыш, даже если остальные запросы по-прежнему требуют участия команды.
При внедрении важно правильно расставить ожидания. SDUI — не универсальное решение, которое закроет все задачи маркетинга. Это инструмент, который перемещает работу с одного места на другое: с конвейера разработки в процесс подготовки конфигурации. И оба процесса требуют дисциплины.
3. Версионная совместимость и старые клиенты. Мобильные приложения обновляются асинхронно. В эксплуатации одновременно находятся версии за последние несколько лет, и часть пользователей может не обновлять приложение годами. Это значит, что новый тип блока, который мы добавили вчера, не понимают приложения старше определённой версии — и если просто отдать им этот блок, то они в лучшем случае его не отобразят, в худшем — упадут.
Мы решаем это с помощью версионирования на уровне модели: у каждого MainPageBlock есть поля version и until_version, ограничивающие диапазон версий приложения, которым этот блок будет отдаваться.
API при сборке ответа фильтрует блоки в соответствии с версией клиента. В результате старые приложения получают только то, что они умеют показывать.
Такой подход требует дисциплины: при добавлении нового типа блока необходимо сразу определять, с какой версии приложения он становится доступен.
При этом подход накладывает ограничение: старые версии приложения никогда не получат новые возможности, даже если эти возможности было бы полезно показать. Однако для особо важных блоков можно делать их упрощённые версии и ограничивать их максимальную версию до минимальной версии нового блока.
4. Классические модульные тесты не спасают от ошибок при заполнении конфигураций. Конфигурация — это данные. Pydantic-схема ловит структурные ошибки, но не поймает семантические:
опечатку в ссылке; битый deeplink на несуществующий экран;
несовместимые сочетания параметров;
неудачное сочетание цветов.
Мы решаем это с помощью нескольких слоёв.
Во-первых, кастомные валидаторы на уровне Pydantic-моделей проверяют не только типы, но и логическую корректность: даты не пересекаются, ссылки соответствуют допустимым экранам, связанные сущности существуют.
Во-вторых, stage-окружение позволяет проверять изменения до вывода в эксплуатацию.
В-третьих, идеальной защиты нет. Часть ошибок можно отследить только глазами, в чём помогает PydanticSchemaRenderService.
5. Границы применимости SDUI. SDUI хорошо закрывает класс задач, где визуал и поведение можно описать как конфигурацию. Однако существуют задачи, для которых этот подход не годится:
для сложных форм с клиентской проверкой (например, корзина или оформление заказа с бизнес-правилами, проверкой остатков и расчётом доставки);
для интерактивов в реальном времени (например, чата поддержки или стриминга);
для сложных анимаций, завязанных на жесты или физику;
для функциональности, которой важна пиксельная точность и специфическое поведение платформы.
Попытка описать в JSON вообще всё приложение требует создавать собственный UI-фреймворк со своей системой типов, рендером и инструментами разработки. Делать его имеет смысл только в том случае, если вы готовы выделить под это отдельную инфраструктурную команду.
История одной ошибки
До появления PydanticSchemaRenderService мы редактировали JSON-конфигурации через Django Admin вручную. Схема была описана в Pydantic, проверка отрабатывала корректно, но сам процесс представлял собой редактирование сырого JSON.
Однажды маркетинг заказчика готовил обновление одного из конфигурируемых блоков. Изменения были рутинные: поправить тексты, обновить ссылки, поменять несколько параметров оформления. Конфигурацию сохранили, проверка прошла без замечаний, изменения ушли в прод.
Спустя некоторое время обнаружилось, что часть полей на экране отображается пустой, несмотря на то, что значения в админке присутствовали.
Разбор занял время. В одном из ключей конфигурации была опечатка: одна перепутанная буква в названии поля. Для Pydantic это означало не ошибку, а отсутствующее поле, и модель честно подставила значение по умолчанию, предусмотренное схемой. Структурно JSON был корректен, проверка прошла, данные сохранились. Но на фронтенд ушли стандартные вместо того, что пытались указать.
Проблему решили после ручного разбора и обновления конфигурации. Инцидент не привёл к критическим последствиям, однако показал, что текущий процесс редактирования допускает ошибки, которые проходят через формальную проверку.
Мы выделили две основные проблемы:
По умолчанию Pydantic игнорирует неизвестные поля в исходных данных. Опечатка в ключе для него — это не ошибка, а просто отсутствующий ожидаемый ключ, который заполняется по умолчанию. Добавили в Pydantic модель
model_config = ConfigDict(extra='forbid')— с ней Pydantic падает с ошибкой проверки на любом незнакомом ключе.Сам формат работы, при котором нетехнический сотрудник редактирует сырой JSON в эксплуатационной админке, по определению хрупкий. Даже с
extra='forbid'остаётся целый класс ошибок, от которых не защищает структурная проверка.
Вывод, к которому мы пришли после этой истории: проблема не в том, что Pydantic что-то не поймал. Проблема в том, что маркетолог вообще видит JSON. Правильное решение — не улучшать проверку ещё одним слоем, а убрать из процесса сам источник таких ошибок: дать человеку не текстовое поле, а форму, в которой ключи заданы схемой и редактируются только их значения. Это и стало задачей, под которую мы сделали описанный выше PydanticSchemaRenderService.
Результаты SDUI
Интерфейсные и контентные изменения, связанные с маркетинговыми сценариями заказчика, теперь занимают минуты вместо дней. По всем фичам, использующим этот механизм, количество изменений достигает порядка 100 в месяц.
Товарные полки, stories и слайдеры создают и редактируют в Django Admin. A/Б-тесты запускают без развёртывания.
В результате команда разработки не тратит время на задачи вроде «поменять цвет» и может сосредоточиться на архитектуре и новых возможностях платформы, пока выполняются изменения интерфейса и контента для маркетинговых сценариев.
Дальнейшее развитие
SDUI — это не разовый проект от заказчика, а развивающийся слой платформы. Каждая новая реализованная фича расширяет возможности команды маркетинга и контент-команды заказчика.
Подход на основе JsonConfigEntry и Pydantic вышел за рамки главной страницы и теперь используется в других частях продукта заказчика — например, в тикерах и всплывающих окнах настройках уведомлений.
PydanticSchemaRenderService фактически выполняет роль визуального конструктора. В планах — предпросмотр результата до публикации прямо в админке, drag-and-drop сортировка блоков, шаблоны для типовых кампаний заказчика, история изменений с возможностью сравнения версий.
Итоги
За пять лет SDUI у нас прошёл путь от оркестратора баннеров до системы из более чем 20 типов блоков, генератора форм по Pydantic-схемам и визуального конструктора. Несколько важных выводов:
Можно начинать без полной архитектуры. SDUI с самого начала не создавали как целостную систему, он складывался итеративно, от конкретных задач. Это нормально и, возможно, даже правильно. Попытка сразу спроектировать универсальный движок с первого дня потребовала бы значительных ресурсов и несла бы высокий риск переусложнения.
SDUI не является универсальным инструментом. Он эффективно закрывает задачи управления контентными блоками, маркетинговыми сценариями интерфейса и A/Б-тестирования визуала. При этом сложные формы, интерактивы, тяжёлые анимации остаются на классическом фронтенде. Попытка описать в JSON абсолютно всё приводит к созданию собственного UI-фреймворка — это отдельный продукт другого масштаба, требующий много ресурсов разработки.
Django Admin может выступать как платформа внутренних инструментов. При добавлении специализированных виджетов вроде color picker, автогенератора форм и кастомных полей под предметную область стандартная админка превращается в полноценный инструмент. Не нужно делать отдельный личный кабинет для каждой роли, можно расширять то, что уже есть.
Если интерфейсные изменения в продукте начинают появляться быстрее, чем проходит цикл доставки кода, то имеет смысл пересмотреть границу между кодом и конфигурацией и вынести часть визуальных и контентных решений в управляемые настройки, не затрагивающие релизный цикл приложения.