$ sudo teach IT

Зачем это нужно

Настоящий ответ сервера редко похож на плоский список полей. Обычно это вложенная структура: у пользователя есть список заказов, у заказа — список товаров, у товара — цена и количество. Часть полей может вообще отсутствовать, если её нечем заполнить. А ключи в 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".