JavaRush /Курсы /Swift SELF /CodingKeys: маппинг имён полей в Codable

CodingKeys: маппинг имён полей в Codable

Swift SELF
59 уровень , 1 лекция
Открыта

1. Зачем нужен CodingKeys, если Swift и так умный?

Когда вы впервые видите Codable, возникает чувство: «О, компилятор сейчас сам всё догадается, я просто назову поля — и готово». И действительно, пока JSON‑ключи совпадают с именами ваших свойств (titletitle), всё выглядит как сказка. Но реальный JSON быстро напоминает, что он живёт по своим традициям: snake_case, странные сокращения, ключи вроде "class" или "user-id", а иногда ещё и версии схемы. В этот момент CodingKeys превращается в ваш переводчик: «в JSON это называется так, а в Swift — нормально и по‑человечески».

Ключевая идея

CodingKeys — это внутренний enum, который описывает соответствие между именами свойств в Swift и ключами в JSON. Как только вы его объявляете, вы берёте контроль над контрактом «как именно это поле называется в данных».

В стандартной модели Codable компилятор умеет синтезировать код, который работает через контейнер «ключ → значение» (container(keyedBy:)) и кодирует/декодирует поля по этим ключам. CodingKeys как раз и есть список таких ключей.

2. Как выглядит CodingKeys и где он живёт

Сейчас будет момент, когда вы скажете: «Опять enum… я же пришёл читать про JSON». Спокойно: CodingKeys — это не «ещё один сложный enum», а просто аккуратный список ключей. Обычно он объявляется внутри вашей структуры (или класса), рядом со свойствами. И именно это делает модель читаемой: открыли тип — сразу видите, как он сериализуется.

Базовый шаблон


import Foundation

struct BookDTO: Codable {
    let id: Int
    let title: String

    enum CodingKeys: String, CodingKey {
        case id
        case title
    }
}

Выглядит скучно? Да. Но это скука правильная: в таком виде CodingKeys не меняет поведение (ключи совпадают), зато даёт вам привычку «держать формат данных рядом с моделью». А когда понадобится переименование — вы добавите одну строку, а не начнёте переписывать всё декодирование вручную.

3. Переименование ключей

Здесь CodingKeys начинает реально спасать. Чаще всего JSON использует snake_case, а Swift по стилю любит camelCase. И мы не хотим называть свойства в Swift как published_year, потому что это выглядит как «код на языке JSON», а не на Swift. Наша цель — чтобы модель читалась как нормальный Swift‑код, а формат хранения был деталью.

Пример: published_year в JSON, но publishedYear в Swift

import Foundation

struct BookDTO: Decodable {
    let title: String
    let publishedYear: Int

    enum CodingKeys: String, CodingKey {
        case title
        case publishedYear = "published_year"
    }
}

let json = #"{"title":"Swift Basics","published_year":2024}"#

do {
    let book = try JSONDecoder().decode(BookDTO.self, from: Data(json.utf8))
    print(book.publishedYear) // 2024
} catch {
    print("Decoding failed:", error)
}

Обратите внимание на приятную вещь: мы не писали ни init(from:), ни ручные контейнеры. То есть «автосинтез» Decodable сохранился — мы просто подсказали ему, как называется ключ.

И ещё одна мысль, которую полезно держать в голове: переименование — это часть формата данных, а не «каприз разработчика». Сегодня в API ключ "published_year", завтра вы захотите "year_published" — и если вы не зафиксировали это в CodingKeys, изменения начнут расползаться по коду.

4. Проблемные JSON‑ключи: "class", "default", "description"

Следующая причина, почему CodingKeys нужен даже без snake_case, — «неудобные» ключи. Иногда JSON приходит от внешней системы, которая считает нормальным ключ "class". А в Swift class — ключевое слово. Да, можно экранировать обратными апострофами, но обычно это не то, чего вы хотите для обычного свойства.

И тут CodingKeys снова выступает переводчиком: «в JSON это "class", а в Swift мы назовём это нормально».

Пример: ключ "class" в JSON

import Foundation

struct CourseDTO: Decodable {
    let className: String

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

let json = #"{"class":"Algorithms"}"#

do {
    let c = try JSONDecoder().decode(CourseDTO.self, from: Data(json.utf8))
    print(c.className) // Algorithms
} catch {
    print("Decoding failed:", error)
}

Да, можно было бы сделать let `class`: String, но это быстро превращает код в «язык с кавычками». CodingKeys позволяет сохранить чистый Swift‑нейминг, не воюя с реальностью входных данных.

5. CodingKeys: что кодируем и что нет

Это одна из тех деталей, которые новички узнают не из учебника, а из боли. В Codable есть правило: список CodingKeys определяет, какие stored properties участвуют в кодировании и/или декодировании.

Практически это означает: если вы не перечислили свойство в CodingKeys, то при автосинтезе оно считается «не часть формата». А дальше начинается важный вопрос: «а как тогда это свойство будет инициализировано при декодировании?»

И здесь появляется правило, которое стоит выучить один раз, чтобы не ловить загадочные ошибки: если поле исключено из JSON, у него должно быть значение по умолчанию или оно должно быть Optional. Это согласуется с общими правилами синтеза Codable: исключённые значения должны быть «как-то заполнены» при Decodable.

Пример: внутреннее поле, которое не хранится в JSON

Представим, что мы читаем книгу из файла, но хотим держать рядом «кэш» для поиска (техническая штука, которую не надо сохранять в JSON).

import Foundation

struct BookDTO: Codable {
    let id: Int
    let title: String

    var cachedTokens: [String] = [] // техническое поле

    enum CodingKeys: String, CodingKey {
        case id
        case title
        // cachedTokens намеренно не участвует
    }
}

let json = #"{"id":1,"title":"Swift CLI"}"#

do {
    let book = try JSONDecoder().decode(BookDTO.self, from: Data(json.utf8))
    print(book.cachedTokens.count) // 0
} catch {
    print("Decoding failed:", error)
}

Здесь cachedTokens успешно инициализируется, потому что у него есть дефолт []. Если бы дефолта не было, компилятор не смог бы «синтезировать» корректную инициализацию при декодировании, и вы бы получили ошибку на этапе компиляции или невозможность синтеза.

6. Практика: файл библиотеки, encode/decode и отладка ключей

Сейчас свяжем тему с нашим курсом: мы пишем CLI‑приложение библиотеки, и где-то рядом уже маячит JSON‑файл, в котором лежит schemaVersion и массив книг. Мы пока не обсуждаем миграции и версии (это отдельная тема), но уже сейчас хотим, чтобы формат JSON был «внешний», а Swift‑модель — «внутренняя и читаемая».

«Файл библиотеки» и читаемый нейминг

Представим, что внешний формат книги выглядит так (условный пример, но очень жизненный):

{
  "id": 10,
  "title": "Swift in Practice",
  "published_year": 2023,
  "author_name": "Ann Lee"
}

А в Swift мы хотим:

  • publishedYear, а не published_year
  • authorName, а не author_name

DTO книги для файла

import Foundation

struct BookFileDTO: Codable {
    let id: Int
    let title: String
    let publishedYear: Int
    let authorName: String

    enum CodingKeys: String, CodingKey {
        case id
        case title
        case publishedYear = "published_year"
        case authorName = "author_name"
    }
}

А теперь завернём это в корневой контейнер «файла библиотеки» (идея контейнера уже была у нас на прошлом дне, сейчас мы только делаем нейминг ключей аккуратным):

import Foundation

struct LibraryFileDTO: Codable {
    let schemaVersion: Int
    let items: [BookFileDTO]

    enum CodingKeys: String, CodingKey {
        case schemaVersion = "schema_version"
        case items
    }
}

Обратите внимание: мы по‑прежнему не пишем ручной init(from:). Мы всё ещё играем в «простую лигу» Codable, просто подстраиваем имена под реальный JSON.

Проверяем, что чтение и запись идут по одним и тем же ключам

Очень частая ловушка новичка: «Я настроил CodingKeys для декодирования, значит всё ок». Но в Codable обычно важно, чтобы тип корректно работал в обе стороны: читаем из файла и записываем обратно.

Хорошая новость: CodingKeys — это единый контракт, и если тип Codable, то эти же ключи будут применяться и при encode(...). То есть вы не получите «одни ключи на чтение, другие на запись» (если не начнёте писать ручную реализацию сами).

Мини‑пример: encode → JSON строка

import Foundation

do {
    let book = BookFileDTO(
        id: 10,
        title: "Swift in Practice",
        publishedYear: 2023,
        authorName: "Ann Lee"
    )

    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

    let data = try encoder.encode(book)
    let text = String(data: data, encoding: .utf8) ?? "<not utf8>"
    print(text)
    // {
    //   "author_name" : "Ann Lee",
    //   "id" : 10,
    //   "published_year" : 2023,
    //   "title" : "Swift in Practice"
    // }
} catch {
    print("Encoding failed:", error)
}

Если вы видите в выводе "published_year", значит ваш маппинг работает «в обе стороны». И это та самая «проверка здравого смысла», которую полезно делать при работе с JSON: один раз вывести закодированный JSON глазами и убедиться, что вы не случайно записываете «не туда».

Как понять, что ключ сломан: читаем DecodingError

Когда ключи не совпадают, JSONDecoder не «подберёт похожее название» и не скажет «ну ладно, я догадался». Он честно упадёт с ошибкой. Это прекрасно: лучше упасть с понятной ошибкой, чем тихо получить неправильные данные.

Давайте нарочно сделаем ошибку: в JSON ключ "published_year", а в CodingKeys мы случайно написали "publish_year" (минус две буквы — плюс 20 минут дебага).

import Foundation

struct BrokenBookDTO: Decodable {
    let publishedYear: Int

    enum CodingKeys: String, CodingKey {
        case publishedYear = "publish_year" // опечатка!
    }
}

let json = #"{"published_year":2024}"#

do {
    _ = try JSONDecoder().decode(BrokenBookDTO.self, from: Data(json.utf8))
    print("Decoded!") // сюда не дойдём
} catch {
    print("Decoding failed:", error)
    // Обычно будет что-то вроде: keyNotFound(...)
}

Смысл такой: если у вас keyNotFound, то почти всегда проблема в одном из трёх мест — ключ не совпал, ключ реально отсутствует, или модель ожидает не‑Optional, а данных нет. CodingKeys помогает с первым, а с остальными мы разберёмся через дисциплину модели: Optional только там, где поле действительно может отсутствовать.

Шпаргалка: частые маппинги через CodingKeys

Иногда полезно иметь «карту местности». Вот короткая таблица, которая покрывает 90% бытовых случаев. Её не нужно заучивать, но полезно узнавать глазами.

Ситуация в JSON Как хочется в Swift Что пишем в CodingKeys
snake_case ключ
camelCase свойство
case myValue = "my_value"
ключ — ключевое слово ("class")
нормальное имя
case className = "class"
поле не должно сохраняться в JSON
техническое поле
не перечисляем ключ; поле делаем Optional или с дефолтом
хотим “переименовать для ясности”
читабельное свойство
case fullName = "name"

Если вы чувствуете, что таблица не помогает, всё нормально: первые пару дней вы будете писать CodingKeys «вслепую». Потом мозг начнёт автоматически видеть паттерны, и вы станете печатать их почти не думая (и это тот редкий случай, когда «почти не думая» — хорошо).

Важный нюанс: CodingKeys — это про формат, а не про бизнес‑логику

Есть соблазн использовать CodingKeys как место, где «мы сейчас наведём порядок в данных», например: «если ключ называется "title", то пусть он автоматически тримится» или «если "author_name" пустой, заменим на "Unknown"». Но это не задача CodingKeys.

CodingKeys отвечает только за «как называются поля и какие из них участвуют». А нормализация и валидация — это отдельные шаги. И это хорошее разделение ответственности: формат данных отдельно, правила домена отдельно. Если смешать это в одну кашу, то через месяц вы сами же будете бояться трогать загрузку JSON, потому что «там какая-то магия».

7. Типичные ошибки при работе с CodingKeys

Ошибка №1: опечатка в строковом ключе и вера в удачу.
Самый обидный баг — когда Swift‑код компилируется, но декодирование падает в рантайме из‑за "publised_year" вместо "published_year". Здесь нет универсального лекарства, кроме дисциплины: держать JSON‑примеры рядом, по возможности один раз прогонять decode/encode на тестовых данных и внимательно читать DecodingError, а не просто печатать «что-то сломалось».

Ошибка №2: исключили поле из CodingKeys, но забыли дефолт или Optional.
Логика простая: если поле не приходит из JSON, откуда ему взяться при создании объекта? Если вы исключаете поле из кодирования/декодирования, позаботьтесь, чтобы оно могло быть инициализировано автоматически — через значение по умолчанию или Optional. Это прямое следствие правил синтеза: исключённые значения должны быть «закрыты» дефолтом при Decodable.

Ошибка №3: пытаются «переименовать JSON», вместо того чтобы «переименовать Swift».
Иногда студент думает: «Ну я же контролирую код, значит я могу просто поменять JSON». В реальности JSON часто приходит извне (API, старый файл, формат задания, чужая база). Правильная стратегия почти всегда обратная: Swift‑модель должна быть удобной вам, а CodingKeys должен адаптировать её под внешний контракт.

Ошибка №4: превращают CodingKeys в мусорный контейнер «всех возможных ключей на будущее».
Хочется дописать case author, case author_name, case writer — «на всякий случай». Но CodingKeys — это контракт, а не фантазия. Если вы добавляете ключи, которых нет в модели, вы запутываете будущего себя. Если вы ожидаете разные варианты входных данных, это уже история про стратегию декодирования или про ручной init(from:) — но это будет позже, не сегодня.

Ошибка №5: забывают, что Codable работает через контейнер «ключ → значение» и требователен к совпадению имён.
Иногда кажется, что декодер «сам найдёт похожее». Нет: Codable в Swift устроен строго, и это его сила. Он кодирует/декодирует поля по ключам контейнера container(keyedBy:), поэтому имена должны совпадать либо напрямую, либо через ваш явный маппинг в CodingKeys. Если совпадения нет — будет ошибка, и это нормально.

1
Задача
Swift SELF, 59 уровень, 1 лекция
Недоступна
Черновик статьи
Черновик статьи
1
Задача
Swift SELF, 59 уровень, 1 лекция
Недоступна
Карточка пользователя
Карточка пользователя
1
Задача
Swift SELF, 59 уровень, 1 лекция
Недоступна
Курс и кэш
Курс и кэш
1
Задача
Swift SELF, 59 уровень, 1 лекция
Недоступна
Витрина библиотеки
Витрина библиотеки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ