1. Зачем нужен CodingKeys, если Swift и так умный?
Когда вы впервые видите Codable, возникает чувство: «О, компилятор сейчас сам всё догадается, я просто назову поля — и готово». И действительно, пока JSON‑ключи совпадают с именами ваших свойств (title → title), всё выглядит как сказка. Но реальный 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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Если вы чувствуете, что таблица не помогает, всё нормально: первые пару дней вы будете писать 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. Если совпадения нет — будет ошибка, и это нормально.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ