Модели данных Swift для грязных и меняющихся ответов API
Модели данных Swift помогают справляться с отсутствующими полями, дрейфом enum и частичными ответами, если разделить строгие проверки, безопасные запасные варианты и логирование.

Почему аккуратные модели ломаются на реальных API
Модель может выглядеть идеальной в Swift и всё равно сломаться при первом же контакте с реальным бэкендом. Проблема простая: приложения ожидают аккуратные данные, а API меняются маленькими, неаккуратными способами. Одного отсутствующего поля, одного переименованного значения или одной необычной формы ответа достаточно, чтобы декодирование не прошло для всего объекта.
Частый пример — карточка товара, которая ожидает title, price и currency. Если сервер пропустит currency для одного элемента, Decodable может отклонить этот элемент целиком, даже если остальные данные в порядке. Пользователям всё равно, что одно поле отсутствует. Они просто видят пустой экран или карточку, которая так и не загрузилась.
Команды бэкенда со временем тоже меняют значения. Статус, который раньше был active или paused, может получить новое значение, например archived. Иногда поле переименовывают, потому что другой сервис использует другое название. Такой дрейф нормален, особенно в быстро меняющихся продуктах, но строгие модели часто воспринимают его как фатальную ошибку.
Один и тот же эндпоинт может даже возвращать разные структуры для разных экранов. Список может получать короткую версию элемента только с полями-сводками. Страница деталей — полный объект. Если ваши модели данных Swift предполагают, что каждый ответ всегда имеет одну и ту же полную структуру, приложение становится хрупким без всякой причины.
Цель не в том, чтобы принимать любой плохой ответ и делать вид, что ничего не произошло. Это лишь скрывает реальные проблемы. Цель — сохранить стабильность приложения и одновременно сделать плохие данные заметными для команды.
Обычно это означает две вещи одновременно: декодировать всё, что можно, и оставлять понятный сигнал, когда сервер присылает что-то неожиданное. Показывайте те части, которые безопасны для отображения. Логируйте неизвестные значения. Используйте заглушки, когда приложение действительно не может угадать. Пользователь получает рабочий экран, а команда всё равно видит несоответствие и может его исправить.
Сразу решите, что приложение не может угадать
Модель становится безопаснее, когда вы перестаёте одинаково относиться ко всем полям. Одни значения обязательны, чтобы экран имел смысл. Другие могут подождать. Если заранее определить эту границу, приложение будет падать контролируемо, а не показывать ерунду.
Начните с полей, которые пользователю обязательно нужны, чтобы понять, что он видит. Для карточки товара это часто id и title. Без id нельзя корректно отслеживать обновления и нажатия. Без title карточка теряет ясный смысл, поэтому показывать её часто вреднее, чем не показывать вовсе.
Другие поля легче сделать более гибкими. Подзаголовок, миниатюра, текст бейджа или краткое описание могут отсутствовать какое-то время. Приложение всё равно может отрисовать карточку с заглушкой, меньшим количеством деталей или более компактным макетом. Это уже решение продукта, а не только кода.
Некоторые поля требуют более строгих правил, потому что приложение никогда не должно их угадывать:
- денежные значения
- значения статуса, которые меняют поведение
- права доступа, определяющие, что может делать пользователь
- даты для биллинга, истечения срока или дедлайнов
Если сервер не прислал цену, не показывайте "0 ₽". Если он не прислал статус, не предполагайте, что это active. Если нет данных о правах, не открывайте действия и не надейтесь на лучшее. Такие догадки создают тихие баги, которые для пользователя выглядят как правда.
В моделях данных Swift это часто означает разделение полей на две группы: обязательные значения, которые должны декодироваться без ошибок, и необязательные значения, которые UI сможет обработать позже. Такое простое разделение делает дизайн модели гораздо понятнее.
Сначала запишите запасной вариант для каждого отсутствующего поля, а уже потом кодите его. Скрыть строку, показать заглушку, отключить кнопку или заблокировать экран с ошибкой. Когда правило записано заранее, Decodable missing fields перестаёт быть сюрпризом и становится предусмотренным сценарием.
Обрабатывайте отсутствующие поля, не пряча проблему
Отсутствующее поле может означать две разные вещи: сервер пропустил что-то неважное или нарушил обещание, от которого зависит приложение. Хорошие модели данных Swift относятся к этим случаям по-разному.
Оставляйте поля необязательными только тогда, когда приложение не может принять безопасное решение без них. id, статус, который используется в логике, или цена для оформления заказа должны проваливать декодирование, если сервер их не прислал. Такой провал полезен. Он показывает, что payload сломан, прежде чем плохие данные расползутся по приложению.
Используйте optional для полей, без которых пользователь может обойтись. Подзаголовок, текст бейджа, промо-метка или второе изображение часто подходят под это правило. Если их нет, экран всё равно может загрузиться, а пользователь всё равно сможет действовать.
Помогает простой тест:
- Нужна ли приложению эта величина, чтобы принять решение?
- Изменит ли подставное значение то, что увидит пользователь, или то, что он сможет сделать?
- Сможет ли экран работать, если этого значения нет?
Если на первые два вопроса ответ «да», поле должно оставаться строгим.
Не подменяйте отсутствующие данные пустыми строками, 0 или фиктивными датами прямо в модели. Такие значения стирают настоящую проблему. Позже уже никто не поймёт, означает ли "" «API прислал пустое значение» или «поле вообще не дошло». Так явный баг превращается в тихий.
Вместо этого добавляйте запасной текст в интерфейсе. Модель может хранить subtitle: String?, а view — показывать "Нет подзаголовка" или скрывать эту строку. Так в модели остаётся исходная правда, а пользователи всё равно видят аккуратный экран.
Также нужно место, где можно смотреть на ошибки. Логируйте ошибки декодирования, по возможности сохраняйте форму payload и отправляйте достаточно деталей в систему ошибок, чтобы команда могла исправить сервер или подстроить клиент. Тихое восстановление приятно во время разработки. В продакшене оно обычно просто скрывает баг на недели.
Оставляйте enum открытыми для новых серверных значений
Серверные enum меняются незаметно. Команда бэкенда добавляет paused или queued, а приложение, которое ожидает только active и archived, может не суметь декодировать весь объект. Для реальных API это слишком хрупко.
В моделях данных Swift enum, зависящие от сервера, обычно должны иметь запасной выход. Оставьте известные кейсы, а затем добавьте один, который хранит исходную строку. Приложение продолжает работать, а логи всё равно показывают точное пришедшее значение.
enum AccountStatus: Equatable, Decodable {
case active
case archived
case unknown(String)
init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self)
switch value {
case "active": self = .active
case "archived": self = .archived
default: self = .unknown(value)
}
}
}
Это лучше, чем пытаться загнать каждое новое значение в старый кейс. Если сервер присылает paused, сопоставление его с .archived сохранит декодирование, но исказит смысл. Такой баг хуже заметного запасного варианта, потому что выглядит правильным, пока клиент не начнёт жаловаться на странное поведение.
Когда приложение видит неизвестное значение, дайте интерфейсу безопасный вариант:
- покажите нейтральную метку вроде "Недоступно"
- используйте обычный стиль вместо предупреждения или успешного состояния
- скройте действия, которые зависят от известного статуса
Небольшой пример помогает. Допустим, карточка товара показывает состояние наличия со стороны сервера. Если бэкенд добавит preorder до выхода обновления приложения, .unknown("preorder") позволит карточке загрузиться, избежать падения и дать команде исходное значение для логов или баг-репортов.
Открытые enum меняют идеальную модель на честную. Обычно это правильный обмен, когда сервер может измениться раньше, чем ваше приложение дойдёт до пользователей.
Проектируйте частичные ответы специально
Многие API не отправляют весь объект сразу. Эндпоинт списка даёт достаточно данных для строки, а эндпоинт деталей дополняет остальное. Если заставить оба ответа жить в одной большой модели, в итоге обычно получается набор optional, и nil перестаёт что-либо значить.
Лучше делать модели меньше и честнее. Используйте одну структуру для строки в списке, другую для карточки-превью и ещё одну для полных деталей. Каждая из них должна соответствовать тому payload, который реально получает.
В магазине список может включать id, name, price и миниатюру. Ответ с деталями — дополнительно описание, остаток и информацию о доставке. Это не один и тот же ответ, значит, и модель не должна притворяться, что это одно и то же.
Так моделям данных Swift проще доверять. Когда поле отсутствует, можно понять, сервер ещё не прислал его или значение действительно отсутствует.
Также нужна состояние экрана, которое умеет собирать эти части во времени. Начните с кратких данных, чтобы экран быстро отрисовался. Когда придут детали, обновите то же состояние по id, а не заменяйте всё целиком.
Хорошо работает простая схема:
ItemSummaryдля списков и результатов поискаItemPreviewдля небольших карточек или связанных элементовItemDetailдля полной страницыItemScreenStateдля отслеживания того, что UI уже получил
Последний пункт особенно важен. UI должен понимать, какие части загружены, какие ещё ждут данных, а какие завершились ошибкой. Если хранить только optional-значения, приложение не различит "пустое описание" и "описание ещё не пришло".
Не нужно ничего усложнять. Даже несколько явных флагов помогают, например detailsLoading, detailsLoaded или detailsError. Если состояние становится сложнее, enum часто выглядит аккуратнее.
Это также помогает, когда API медленный или отвечает неравномерно. Пользователь может листать список, открывать экран и сразу видеть заголовок, пока остальная часть подгружается через мгновение. Это ощущается нормально. Пустые области, которые выглядят как будто уже готовы, — нет.
Одна частая ошибка — декодировать полную модель из каждого эндпоинта, потому что так кажется аккуратнее. Но при изменении API это уже не аккуратно. Более маленькие модели лучше подходят для частичных ответов API и помогают быстрее замечать плохие данные.
Разделяйте сырые данные API и данные экрана
Server payload и модель экрана не должны быть одним и тем же типом. Payload отражает то, что API прислал сегодня. Экранная модель отражает то, что приложение может безопасно показать человеку.
Сначала декодируйте в сырые структуры ответа. Пусть эти типы будут как можно ближе к формату провода: optional-поля, нестрогие строки, серверные ID, неизвестный текст статуса, даже странные значения, которые выглядят неправильно. Если API однажды пришлёт price: "free", а в другой раз price: 19.99, сырой слой должен сохранить этот хаос, а не заставлять UI с ним разбираться.
Затем преобразуйте сырые данные в более компактную экранную модель с чёткими правилами. На этом шаге вы решаете, что действительно валидно, что получает запасной вариант и что должно заблокировать отображение. Держите эти решения в одном месте. Если каждый view добавляет свои проверки, приложение начинает показывать одни и те же данные по-разному, а баги становится трудно отследить.
Простой маппер обычно отвечает на четыре вопроса:
- Может ли приложение вообще показать этот элемент?
- Какие поля обязательны для экрана?
- Какие плохие значения получают запасной вариант?
- Какие подозрительные значения стоит сохранить для последующей проверки?
Последний пункт особенно важен. Не выбрасывайте странные серверные значения слишком рано. Сохраняйте их в сырой модели или, если нужно, храните рядом с преобразованным результатом. Если статус вернулся как "paused_temp", а ваше приложение знает только active и paused, сохраните исходный текст, чтобы команда могла посмотреть логи, воспроизвести проблему и обновить маппер.
Такое разделение делает модели данных Swift понятнее. Представления остаются простыми, валидация — одинаковой, а плохие данные API остаются заметными вместо того, чтобы тихо превращаться в бессмыслицу на экране.
Постройте более безопасную модель шаг за шагом
Начните с малого. Выберите один эндпоинт и один экран, который от него зависит, например сводку заказа или страницу аккаунта. Так работа остаётся понятной, и сразу видно, справляется ли модель с реальным хаосом API, а не только с JSON без ошибок.
Сначала сделайте сырой Decodable-модель, которая как можно точнее соответствует серверу. Не переходите сразу к свойствам, готовым для экрана. Сначала решите, какие поля приложению действительно нужны, а какие могут отсутствовать какое-то время.
Помогает простое правило:
- обязательные поля должны существовать, иначе приложение не сможет показать экран корректно
- необязательные поля могут отсутствовать, и экран всё равно покажет что-то честное
Особенно это важно для enum. Если завтра сервер добавит новый статус, закрытый enum может сломать декодирование всего объекта. Используйте кейс unknown(String), чтобы модель сохранила исходное значение, а не падала и не делала вид, что ничего не произошло.
Дальше напишите тесты, которые намеренно ломают payload. Уберите обязательное поле. Измените строковый enum на новое значение. Отправьте только половину ответа. Хорошие модели данных Swift не считают это редкостью. Они считают это обычным событием.
Потом преобразуйте сырую модель в UI-модель в одном месте. Запасные варианты добавляйте именно там, а не в процессе декодирования. Если экран показывает "Неизвестный отправитель" или скрывает бейдж, это решение продукта. Декодирование должно честно отражать то, что сервер реально прислал.
Логирование замыкает цикл. Когда декодирование не проходит, логируйте, какое поле сломалось и какой эндпоинт это прислал. Когда встречается неизвестное значение enum, логируйте и его, и считайте такие случаи. Один странный payload — шум. Один и тот же сбой 300 раз в день — это уже реальный баг на клиенте или на сервере.
Подход простой, но он работает. Вы начинаете с одного экрана, отмечаете обязательные поля, оставляете enum открытыми, тестируете плохие payload, преобразуете сырые данные в UI-данные и смотрите логи. После этого следующий эндпоинт становится намного проще.
Карточка товара, которая загружается по этапам
Карточке товара не нужно все поля с первого ответа. Ей нужно только достаточно данных, чтобы оставаться полезной.
Допустим, приложение сначала получает вот это:
{ "name": "Trail Bottle", "price": "24.00" }
Этого достаточно, чтобы нарисовать карточку с названием и ценой. Приложение не должно падать из-за того, что stock и badge отсутствуют. Но и придумывать их оно не должно. Если остаток неизвестен, карточка может просто не показывать эту строку или вывести обычное сообщение "Проверяем наличие".
Позже приложение запрашивает больше деталей и получает вот это:
{ "stock": 3, "badge": "flash_drop" }
Теперь карточка становится более полной. Появляется остаток: "3 шт. осталось". Сложный момент — badge. Возможно, приложение знает только sale и new, но сервер только что добавил flash_drop. Если Badge — закрытый enum, декодирование сломается, и вы потеряете всё обновление. Это плохой обмен.
Более безопасная модель сохраняет исходное значение:
enum Badge: Equatable {
case sale
case new
case unknown(String)
}
С такой структурой карточка всё равно обновит остаток. Для бейджа приложение может показать нейтральную метку, сохранить исходный текст для логов и не делать вид, что новое значение уже понято. Это важно. Тихо подставить .sale или скрыть поле — значит усложнить отладку.
Итог простой: пользователи сразу видят название товара и цену, потом — остаток, когда он приходит, а приложение честно относится к бейджу, который пока не распознало. Именно так и должны работать устойчивые модели данных Swift.
Ошибки, которые создают тихие баги данных
Падение обычно чинят быстро. Тихий баг данных может жить неделями, потому что приложение всё ещё открывается и выглядит нормально. Самые опасные ошибки — те, что сглаживают плохие данные сервера и делают их похожими на обычные.
Первая ловушка — подставлять пустое значение в каждое отсутствующее поле. Если превратить отсутствующую строку в "", число — в 0, а массив — в [], вы уничтожаете следы проблемы. Товар без цены — это не то же самое, что бесплатный товар. Неизвестный остаток — это не то же самое, что ноль. Хорошие модели данных Swift держат эту разницу видимой, а потом уже позволяют приложению решить, что показывать.
Ещё одна ошибка — использовать одну огромную модель для всех экранов. Список, страница деталей и форма редактирования часто получают разные поля от разных эндпоинтов. Когда они все делят один тип Decodable, появляются десятки optional и мелкие исправления на уровне view, разбросанные по всему приложению. Так два экрана начинают показывать разную правду об одном и том же объекте.
Enum часто ломаются тише. Сервер добавляет новое значение вроде "paused", ваше приложение знает только "active" и "disabled", и кто-то считает любое другое значение невозможным. Если декодирование молча сопоставит новое значение со старым кейсом, UI начнёт врать. Оставьте кейс unknown(String), чтобы сохранить исходное значение.
Перехват ошибок декодирования и молчаливое их игнорирование приводит к тому же ущербу. Приложение продолжает работать, но никто не знает, какое поле сломалось и как часто это происходит. Логируйте сбой, при необходимости показывайте безопасный запасной вариант и держите плохие данные видимыми для команды.
Код представления не должен придумывать правила данных на ходу. Если один экран считает отсутствующую дату "сегодня", а другой — "не запланировано", пользователи получают противоречия. Вынесите эти правила в один слой преобразования, а не в случайные views.
Быстрая проверка перед релизом
Большинство багов перед релизом начинаются с одной тихой предпосылки: сервер и завтра будет присылать ту же структуру, что и сегодня. Обычно это не так. Короткая проверка перед выпуском помогает поймать случаи, из-за которых модели данных Swift выглядят нормально в тестах, но ломаются на реальных устройствах.
Прогоняйте несколько плохих payload перед каждым релизом, а не только happy path. Нужны доказательства, что декодирование падает громко, когда должно, остаётся гибким, когда может, и никогда не подсовывает выдуманные данные в UI.
- Уберите одно несущественное поле из тестового ответа. Экран всё равно должен отрисоваться, если это поле действительно необязательное. Если без него нельзя, пометьте поле как обязательное и считайте сбой реальным.
- Добавьте одно новое значение enum со стороны сервера, например статус, который вы раньше не видели. Тесты должны подтвердить, что декодирование всё ещё работает, а приложение сохраняет или показывает неизвестное значение вместо падения.
- Специально испортите одно поле, например отправьте текст там, где должно быть число. Логи должны показать, какое поле сломалось, что пришло в ответ и какой запрос это вызвал.
- Загрузите на экран частичный ответ API. UI должен показать заглушки, отключённые действия или пустые состояния. Он не должен выдумывать цену, рейтинг, дату или количество на складе.
- Проверьте, что в отчётах об ошибках остаётся достаточно деталей payload для последующей отладки, не валя приложение и не засоряя логи шумом.
Небольшой пример помогает: если ответ о товаре приходит без URL изображения и с новым статусом доступности, карточка всё равно должна загрузить название и цену, показать заглушку изображения и не гадать, что означает новый статус.
Это и есть стандарт, к которому стоит стремиться при обработке отсутствующих полей в Decodable и дрейфа enum в Swift. Если один плохой ответ всё ещё может показать, что именно сломалось, ваше приложение в гораздо лучшем состоянии.
Следующие шаги для вашей команды
Не нужно чинить сразу все эндпоинты. Выберите один, который уже вызывает проблемы, например карточку товара, профиль пользователя или сводку заказа, и сначала приведите в порядок эту модель. Один хороший проход по нестабильному эндпоинту часто даёт команде правила, которые потом можно использовать во всех моделях данных Swift.
Начните с видимости, а не с ещё большего количества fallback-значений. Если декодирование падает, поле исчезает или сервер присылает новый кейс enum, логируйте это. Тихие значения по умолчанию кажутся безопасными неделю, а потом превращаются в баги, которые никто не может отследить.
Короткий разбор с командой полезнее, чем ещё одна длинная спецификация. Сядьте вместе с бэкенд-командой и опишите каждое поле простыми словами:
- какие поля приложению нужны, чтобы отрисовать экран
- какие поля могут отсутствовать короткое время
- какие значения enum могут расшириться позже
- что приложение должно показывать, когда данные приходят только частично
Сделайте эти правила настолько простыми, чтобы новый разработчик мог прочитать их за две минуты. Если правило звучит расплывчато, код почти всегда получится таким же.
Потом превратите решения в тесты. Добавьте один пример payload с отсутствующими полями, один с неизвестным значением enum и один частичный ответ, который всё равно должен показать что-то полезное. Сначала выпустите это изменение модели, посмотрите логи, а потом переходите к следующему нестабильному эндпоинту.
Если вашей команде нужен второй взгляд перед более широким запуском, Oleg Sotnikov может провести обзор контрактов API, моделей Swift и плана внедрения в рамках точечной консультации. Такой обзор часто дешевле, чем потом искать тихие баги данных после релиза.