Зачем это нужно
Настоящий ответ сервера редко похож на плоский список полей. Обычно это вложенная
структура: у пользователя есть список заказов, у заказа — список товаров, у товара —
цена и количество. Часть полей может вообще отсутствовать, если её нечем заполнить.
А ключи в JSON почти всегда пишут snake_case, потому что так принято
в большинстве бэкендов, тогда как в Swift переменные называют camelCase.
Разобраться, как читать такие ответы и что делать, если формат немного не совпал
с ожиданиями, — обязательный навык для любой работы с внешними данными.
Вложенные структуры
Если в JSON есть массив объектов, в Swift ему соответствует массив структур,
подписанных под Decodable. Структуры можно вкладывать друг в друга
так же, как вложены сами данные:
struct OrderItem: Decodable {
let name: String
let price: Int
let quantity: Int
}
struct Order: Decodable {
let id: Int
let items: [OrderItem]
}
Декодер сам пройдёт по каждому элементу массива items и соберёт из него
массив OrderItem. Вам не нужно писать цикл вручную — достаточно правильно
описать форму данных структурами.
snake_case и keyDecodingStrategy
Когда ключи в JSON — full_name или is_active, а в Swift
хочется fullName и isActive, есть два пути. Первый вы уже
видели — свои CodingKeys под каждое поле. Второй короче, если во всём
ответе один и тот же стиль имён: сказать декодеру переводить их автоматически.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
struct User: Decodable {
let fullName: String
let isActive: Bool
}
С такой настройкой декодер сам поймёт, что ключ full_name нужно
положить в свойство fullName, а is_active — в
isActive. Писать CodingKeys для каждого поля больше не нужно.
Правило простое: буква после подчёркивания становится заглавной, а само подчёркивание
исчезает.
Необязательные поля
Если поле в ответе иногда отсутствует или бывает null, в структуре
объявляйте его как опционал:
struct User: Decodable {
let fullName: String
let email: String?
}
Для опционального свойства декодер не требует, чтобы ключ обязательно присутствовал:
если ключа email в JSON нет вообще или он равен null,
fullName об этом даже не узнает — email просто станет
nil. А вот если убрать знак вопроса у необязательного в реальности поля,
декодирование упадёт при первом же ответе, где это поле отсутствует.
Ошибки разбора: DecodingError
Когда форма JSON не совпадает со структурой, decode бросает ошибку
типа DecodingError — это перечисление с несколькими случаями, и по нему
можно понять, что именно пошло не так, не читая сырой текст исключения:
do {
let order = try JSONDecoder().decode(Order.self, from: data)
print(order.id)
} catch let DecodingError.keyNotFound(key, _) {
print("Не хватает поля: \(key.stringValue)")
} catch let DecodingError.typeMismatch(_, context) {
print("Неверный тип в поле: \(context.codingPath.last?.stringValue ?? "?")")
} catch {
print("Не удалось разобрать JSON")
}
DecodingError.keyNotFound — в JSON нет обязательного ключа.
DecodingError.typeMismatch — тип значения не совпал с типом свойства,
например пришла строка там, где структура ждёт число. DecodingError.valueNotFound
— значение оказалось null там, где свойство не опционал.
DecodingError.dataCorrupted — сам текст вообще не является корректным JSON.
У каждого случая, кроме последнего, есть codingPath — путь до проблемного
поля, по нему легко построить понятное сообщение вместо технического текста исключения.
Частые ошибки
- Забыть про
?у поля, которое в реальных ответах иногда отсутствует — декодирование будет падать непредсказуемо на части ответов. - Включить
.convertFromSnakeCase, а часть ключей в JSON и так ужеcamelCase— тогда преобразование сломает именно их. Стратегия применяется ко всему ответу целиком, смешивать стили в одном JSON не стоит. - Ловить только общий
catchи печатать техническое сообщение об ошибке — пользователь или коллега, читающий лог, не поймёт, какое поле виновато. - Перепутать порядок вложенности: если структура повторяет форму JSON не один в один, декодер укажет на несуществующее с виду поле — на самом деле промахнулось имя вложенной структуры.
Резюме
- Вложенный JSON описывается вложенными структурами
Decodable, массивы — массивами структур. decoder.keyDecodingStrategy = .convertFromSnakeCaseизбавляет от ручныхCodingKeys, когда все ключи в одном стиле.- Необязательные и переменчивые поля объявляйте опционалами — тогда их отсутствие не ломает разбор всего ответа.
DecodingErrorразличает нехватку ключа, несовпадение типа и пустое значение — используйте это, чтобы показывать понятную причину, а не сырой текст ошибки.
Проверьте себя
3 вопроса
Активные пользователи из ответа сервера
Сервер прислал JSON со списком пользователей. У каждого пользователя есть имя full_name, флаг is_active и необязательный email (его может не быть вовсе или он может быть null). Реализуйте функцию activeUserNames(_ json: String) -> [String], которая разбирает такой JSON и возвращает имена только активных пользователей, в том порядке, в котором они шли в исходном списке. Ключи JSON в стиле snake_case — переводите их в camelCase через настройку декодера, а не вручную. Если строка вообще не является корректным JSON, верните пустой массив.
Понятное сообщение об ошибке разбора заказа
Заказ в JSON выглядит так: у него есть id, список items (у каждого товара name, price и quantity) и необязательная discount. Реализуйте функцию describeOrder(_ json: String) -> String. При успешном разборе верните строку вида "Заказ 1: итого 1150", где итог — это сумма price * quantity по всем товарам минус скидка (если скидки нет, вычитать нечего). Если в JSON не хватает ключа items, верните "Не хватает поля: items" (имя поля берите из самой ошибки, а не пишите его в коде напрямую). Если у какого-то товара тип значения не совпал с ожидаемым (например цена пришла строкой), верните "Неверный тип в поле: price" (имя поля тоже берите из ошибки). В остальных случаях, когда JSON вообще не разбирается, верните "Не удалось разобрать JSON".