Однажды для реализации одной задачи мне понадобилось декодировать сложносоставленый файл JSON, с многоуровненной вложенностью. Ниже хочу описать, как можно реализовать чтение подобных файлов.
Прежде чем взяться за самое сложное, давайте сначала глянем на базу. Например, возьмём простой файл описания фильмов:
{
"title": "Back to the Future",
"url": "https://www.rombik.dev/blog/back-to-the-future/",
"category": "scifi"
}
Чтобы декодировать его нам необходимо указать, что структура использует протокол Decodable:
struct Movie: Decodable {
// Укажем что перечисление так же соответствует протоколу Decodable
enum Category: String, Decodable {
case scifi, comedy
}
let title: String
let description: String?
let url: URL
let category: Category
}
Все свойства соответствуют полям из нашего JSON файла и так как указано использование декодируемого протокола, они автоматически будут преобразованы. Свойство description указано как необязательное, это означает что в получаемом файле это поле может быть не заполнено. Тогда при декодировании у нас не возникнет ошибки исполнения.
Используя JSONDecoder, мы можем провести анализ JSON файла всего парой строк кода:
let jsonData = JSON.data(using: .utf8)!
let movie: Movie = try! JSONDecoder().decode(Movie.self, from: jsonData)
print(movie.title) // Выведет: "Back to the Future"
Порой название полей в файле JSON могут отличатся он наименования свойств в нашей структуре. Чтобы не менять наименования свойств и соответствовать соглашению CamelCase используем сопоставление ключей JSON. Допустим в нашем файле добавлено поле с именем publish-date и нам его надо сопоставить с нашим свойством. Для этого добавим в структуру перечисление CodingKeys с описанием полей:
struct Movie: Decodable {
enum Category: String, Decodable {
case scifi, comedy
}
enum CodingKeys: String, CodingKey {
// Не забываем указывть все ключи, иначе будет ошибка декодирвания
case title, description, url, category
// Сопоставим ключ JSON с нашим свойством "publishDate"
case publishDate = "publish-date"
}
let title: String
let description: String?
let url: URL
let category: Category
let publishDate: String?
}
А теперь самое интересное! 😀 Добавим в исходные данные массив, в котором будут хранится имена актёров, а в данных актеров ссылка на дополнительную информацию. Обозначим новые модели для актёров и дополнительной информации, а так же добавим метод init(from decoder: Decoder) где укажем для декодера описание типов для каждого из полей.
struct Actor: Decodable {
enum CodingKeys: String, CodingKey {
case name
case birthday
case additional
}
let name: String
let birthday: String
// Укажем тип, описывающий дополнительнуюю информацию
let additional: AdditionalInfo?
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
// Попытка декодировать данные и сопоставить с ключами
name = try values.decode(String.self, forKey: .name)
birthday = try values.decode(String.self, forKey: .birthday)
// Поптыка декодировать не обязательное поле, если оно заполнено
additional = try values.decodeIfPresent(AdditionalInfo.self, forKey: .additional)
}
init(name: String, birthday: String, additional: AdditionalInfo?) {
self.name = name
self.birthday = birthday
self.additional = additional
}
}
struct AdditionalInfo: Decodable {
enum CodingKeys: String, CodingKey {
case city
case country
}
let city: String
let country: String
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
city = try values.decode(String.self, forKey: .city)
country = try values.decode(String.self, forKey: .country)
}
init(city: String, country: String) {
self.city = city
self.country = country
}
}
Добавим новое свойство в нашу первоначальную модель:
struct Movie: Decodable {
...
let publishDate: Date?
// Необязательный массив с типом Actor
let actors: [Actor]?
}
Осталось дело за чтением файла JSON и его окончательным декодированием.
let JSON = """
{
"title": "Back to the Future",
"url": "https://www.rombik.dev/",
"category": "scifi",
"actors": [{
"name": "Michael J. Fox",
"birthday": "June 9, 1961",
"additional": {
"city": "Edmonton",
"country": "Canada",
}
},
{
"name": "Christopher Lloyd",
"birthday": "October 22, 1938",
"additional": {
"city": "Stamford",
"country": "U.S. state",
}
}]
}
"""
let jsonData = JSON.data(using: .utf8)!
// создаем переменную для декодера
let decoder = JSONDecoder()
do {
// указываем декодеру модель Movie
let parsedData = try decoder.decode(Movie.self, from: jsonData)
// выводим на печать наш итоговый результат
print(parsedData)
} catch {
// выводим ошибку, если не удалось выполнить декодирование
print(error.localizedDescription)
}
Таким образом дополняя и усложняя структуры можно разобрать на составляющие JSON файлы любой сложности и вложенности.