JavaRush /Курси /Swift SELF /keyDecodingStrategy (convertFromSnakeCase)

keyDecodingStrategy (convertFromSnakeCase)

Swift SELF
Рівень 59 , Лекція 3
Відкрита

1. Проблема snake_case проти camelCase

Коли ви пишете Swift-код, майже завжди використовуєте camelCase: createdAt, bookId, schemaVersion. Це не примха — так працюють мова та стандартна бібліотека. Натомість у JSON часто трапляється snake_case: created_at, book_id, schema_version.

Проблема виникає просто: ваш тип очікує ключ createdAt, а JSON надсилає created_at. У цей момент JSONDecoder чесно каже: «Я не телепат» — і кидає помилку keyNotFound або typeMismatch. Тобто структура даних начебто правильна, але договір про назви ключів порушено.

Щоб побачити конфлікт якнайнаочніше, достатньо такого прикладу.

import Foundation

struct Profile: Decodable {
    let firstName: String
}

let json = #"{"first_name":"Ann"}"#
let data = Data(json.utf8)

do {
    _ = try JSONDecoder().decode(Profile.self, from: data)
    print("Гаразд")
} catch {
    print("Декодування не вдалося:", error) // Декодування не вдалося: ...
}

З погляду студента це виглядає як «ну майже ж збіглося!». З погляду декодера це виглядає як: «ключі різні, отже контракт не виконано».

keyDecodingStrategy і чим вона відрізняється від CodingKeys

Коли ви вперше знайомитеся з Codable, легко подумати: «Раз ключі різні, значить завжди пишемо CodingKeys». Це робочий підхід, але він починає дратувати, коли перед вами вже двадцятий JSON підряд із тим самим правилом: усі ключі — snake_case, а я хочу camelCase. Писати 20 рядків case createdAt = "created_at" — це не програмування, а переписування словника.

keyDecodingStrategy — це налаштування декодера, яке каже: «Якщо ключі в JSON не збігаються з очікуваними, спробуй застосувати правило перетворення». Це не заміна CodingKeys, а інший інструмент: CodingKeys — точкове налаштування всередині типу, а keyDecodingStrategy — глобальне правило на рівні JSONDecoder.

По суті, це продовження тієї самої ідеї, що й CodingKeys: ми хочемо, щоб перетворення з «сирих даних» у «суворо типізовані дані» залишалося максимально безпечним і передбачуваним, але без зайвої рутини.

Важливо: keyDecodingStrategy впливає саме на декодування — читання JSON у Swift. Вона не лікує енкодер і не змінює те, як ви записуватимете JSON назад.

2. .convertFromSnakeCase

Найпопулярніша стратегія — .convertFromSnakeCase. Вона саме це й робить: перетворює ключі на кшталт first_name на очікуване firstName.

Мінімальний робочий приклад — буквально два рядки налаштування.

import Foundation

struct Profile: Decodable { let firstName: String }

let json = #"{"first_name":"Ann"}"#
let data = Data(json.utf8)

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let p = try decoder.decode(Profile.self, from: data)
    print(p.firstName) // Ann
} catch {
    print("Декодування не вдалося:", error)
}

Тут зручно те, що модель залишається «свіфтовою», без JSON-сміття, а правило перетворення живе в одному місці — там, де ми налаштовуємо декодер.

Працює і для вкладених об’єктів

snake_case зʼявляється не лише на верхньому рівні. Вкладені об’єкти часто приносять ще більше таких ключів, і якби стратегія працювала тільки зовні, вона була б майже марною. Добра новина: вона застосовується і до вкладених об’єктів.

import Foundation

struct AuthorDTO: Decodable { let fullName: String }
struct BookDTO: Decodable { let title: String; let author: AuthorDTO }

let json = #"{"title":"Swift","author":{"full_name":"Ann Lee"}}"#
let data = Data(json.utf8)

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let book = try decoder.decode(BookDTO.self, from: data)
    print(book.author.fullName) // Ann Lee
} catch {
    print("Декодування не вдалося:", error)
}

Це як автоперекладач, який працює не лише із заголовком листа, а й з усіма вкладеними цитатами.

3. Як саме перетворюються ключі, і чому іноді виходить «не те»

На цьому місці зазвичай хочеться сказати: «Гаразд, це магія, рухаємось далі». Але магія любить брати платню за обслуговування, тому корисно розуміти типові наслідки.

.convertFromSnakeCase добре працює у «нормальному світі», де ключі виглядають як schema_version, created_at, book_id. Тоді ви отримуєте schemaVersion, createdAt, bookId. Можна уявити правило так: декодер розбиває рядок за _, бере першу частину як є, а решту перетворює на слова з великої літери й склеює.

Нижче — маленька таблиця-шпаргалка (логіка «за змістом», щоб ви розуміли очікування):

JSON ключ (snake_case) Swift-властивість (очікуване)
book_id
bookId
created_at
createdAt
schema_version
schemaVersion
cover_url
coverUrl
is_read
isRead

У навчальному коді це відчувається як «поставив прапорець — і все ожило». Але далі починаються кути кімнати, об які всі хоча б раз обов’язково вдаряються.

Ключі з дефісами, пробілами та іншими «радощами»

.convertFromSnakeCase не перетворює ключ user-id на userId. Бо це вже не snake_case, а «хтось влаштував дискотеку з пунктуацією». У такому разі декодер не здогадається, тож знову знадобиться CodingKeys.

import Foundation

struct Payload: Decodable {
    let userId: Int

    enum CodingKeys: String, CodingKey {
        case userId = "user-id"
    }
}

Зарезервовані слова і «погані» ключі на кшталт class

Якщо в JSON ключ називається class, а ви не хочете називати властивість class (бо Swift теж хоче жити), стратегія вам не допоможе. Це не про стиль, а про зміст і зручність API. Тут теж потрібен CodingKeys.

import Foundation

struct LessonDTO: Decodable {
    let className: String

    enum CodingKeys: String, CodingKey {
        case className = "class"
    }
}

Колізії: коли різні ключі перетворюються на одне й те саме імʼя

Це рідкісна, але дуже неприємна ситуація. Уявіть JSON, де є і my_key, і myKey. Після перетворення обидва починають виглядати як myKey. У найкращому разі ви отримаєте «не ті дані», у найгіршому — декодування впаде або поводитиметься неочевидно. Реальні API так робити не повинні, але іноді «не повинні» й «не роблять» — це різні релігії.

Саме тому стратегія хороша, коли у вас послідовний формат даних, а не «все підряд в одному JSON».

4. Як поєднувати .convertFromSnakeCase з CodingKeys

Найзріліший і найпрактичніший стиль такий: вмикаємо .convertFromSnakeCase як правило «за замовчуванням», а CodingKeys використовуємо як аптечку — лише для винятків. Тоді ваша модель не перетворюється на довідник того, як цей конкретний API називає поля, а залишається нормальною Swift-моделлю.

Щоб зафіксувати думку, зручно тримати в голові таку схему ухвалення рішення:

flowchart TD
    A[Ключі JSON збігаються з camelCase?] -->|Так| B[Можна без стратегії й без CodingKeys]
    A -->|Ні, але це snake_case| C[Увімкнути .convertFromSnakeCase]
    C --> D{"Є нестандартні ключі? (дефіси, class, дивні імена)"}
    D -->|Ні| E[Живемо спокійно]
    D -->|Так| F[Додаємо точковий CodingKeys лише для винятків]

Тепер приклад, де стратегія допомагає для created_at, а CodingKeys потрібен лише для user-id.

import Foundation

struct Payload: Decodable {
    let createdAt: String
    let userId: Int

    enum CodingKeys: String, CodingKey {
        case createdAt
        case userId = "user-id"
    }
}

Якщо ви налаштуєте декодер так:

import Foundation

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

то created_at потрапить у createdAt автоматично, а user-id буде оброблено вручну через CodingKeys. Виходить саме те, що потрібно: мінімум шуму в коді й максимум контролю там, де він справді потрібен.

5. Супутні ефекти в LibraryCLI: тримаємо правила в одному місці

Тепер прив’яжімо це до нашого навчального застосунку. За курсом ми будуємо CLI-утиліту LibraryCLI, яка зберігає бібліотеку книг у JSON. У попередній лекції ми вже обговорювали ідею кореневого контейнера файлу (умовно LibraryFile { schemaVersion, items }). Сьогодні припустімо, що формат зберігання або зовнішнє джерело раптом перейшло на snake_case, наприклад schema_version замість schemaVersion.

Якщо розв’язувати проблему на місці — у кожному decode(...) — з’являться десятки різних декодерів: один зі стратегією, другий без неї, третій із «трохи іншим» набором налаштувань. Це майже гарантований шлях до багів формату, які проявляться не відразу.

Набагато краще завести маленьку фабрику декодера і вважати її єдиною точкою істини для формату JSON.

import Foundation

func makeLibraryDecoder() -> JSONDecoder {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return decoder
}

Тепер будь-який код, що читає JSON, використовує один і той самий контракт. Наприклад, наш файл:

import Foundation

struct LibraryFileDTO: Decodable {
    let schemaVersion: Int
    let items: [BookDTO]
}

А тестовий JSON (у реальному проєкті це буде Data з файлу, але сьогодні ми працюємо в пам’яті):

import Foundation

let json = """
{ "schema_version": 1, "items": [] }
"""
let dto = try makeLibraryDecoder().decode(LibraryFileDTO.self, from: Data(json.utf8))
print(dto.schemaVersion) // 1

Зверніть увагу на приємний ефект: модель LibraryFileDTO залишається нормальною (camelCase), а формат зберігання (snake_case) описується на рівні налаштування декодера. Це психологічно правильний поділ: тип описує дані, а декодер — контракт серіалізації.

Чому це особливо важливо, коли в проєкті є й інші стратегії

Навіть якщо в цій лекції ми зосереджуємося на ключах, на практиці поруч майже завжди живуть і інші налаштування: наприклад, стратегія дат. У попередній лекції ми говорили, що запис і читання дати мають бути симетричними, інакше ви отримаєте або помилку, або «дату з паралельного всесвіту».

Якщо ви тримаєте декодер в одному місці, ви можете одночасно зафіксувати обидва правила: «ключі snake_case» і «дати ISO-8601» (або ті, що у вас за контрактом). Це робить поведінку програми повторюваною, а баги — відтворюваними, що в програмуванні майже як суперсила.

6. Типові помилки під час використання keyDecodingStrategy

Помилка № 1: увімкнути .convertFromSnakeCase й чекати, що вона виправить будь-які «не такі» ключі.
Стратегія вміє працювати саме зі snake_case. Ключі з дефісами, пробілами, крапками та іншими символами вона не перетворює в потрібний формат. Якщо JSON надсилає user-id або user.name, це вже історія для CodingKeys, а не для стратегії.

Помилка № 2: налаштувати декодер в одному місці, а потім випадково створити інший JSONDecoder() без налаштувань.
Це дуже частий баг: в одному файлі у вас makeLibraryDecoder(), а в іншому — «та ну, тут один раз, що може статися» — і ви пишете JSONDecoder().decode(...). Потім хтось змінює формат ключів, і половина застосунку читає JSON нормально, а половина падає. Лікується дисципліною: один декодер — один формат.

Помилка № 3: думати, що .convertFromSnakeCase впливає на кодування (JSONEncoder).
Вона впливає лише на декодування. Тобто ви можете прочитати schema_version, отримати schemaVersion, а під час запису назад раптом отримати JSON із ключем schemaVersion (camelCase). Якщо формат зберігання має залишатися snake_case, це окреме завдання й окремі налаштування (ми сьогодні свідомо не заглиблюємося в це).

Помилка № 4: помітити колізію ключів і довго не розуміти, чому дані поводяться дивно.
Якщо вхідний JSON містить одночасно my_key і myKey, після перетворення обидва стають схожими на одне й те саме ім’я. Це створює неочевидну поведінку. Якщо ви бачите, що дані ніби перезаписалися, насамперед перевірте вхідний JSON на такі дублікати й пам’ятайте, що стратегія змінює ключі перед зіставленням.

Помилка № 5: ускладнювати модель CodingKeys там, де стратегія розв’язує проблему простіше.
Іноді студенти, дізнавшись про CodingKeys, починають писати їх завжди — навіть для чистого snake_case. У підсумку типи перетворюються на шум, де половина коду повторює очевидне. keyDecodingStrategy існує саме для того, щоб цього шуму не було, тому використовуйте її як правило за замовчуванням, коли формат справді регулярний.

1
Задача
Swift SELF, 59 рівень, 3 лекція
Недоступна
Профіль із API
Профіль із API
1
Задача
Swift SELF, 59 рівень, 3 лекція
Недоступна
Каталог мінібібліотеки
Каталог мінібібліотеки
1
Задача
Swift SELF, 59 рівень, 3 лекція
Недоступна
Книга та автор
Книга та автор
1
Задача
Swift SELF, 59 рівень, 3 лекція
Недоступна
Змішані ключі
Змішані ключі
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ