Знайдіть ідеальний парсер JSON для CoreData - decodable від Apple

Привіт, дорогі колеги та ті, хто працює з iOS. Якщо ви маєте базове уявлення про протокол Decodable, ви зможете розпарсити простий JSON за хвилину, і багато сайтів мають численні приклади того, як парсити JSON. Проблеми починаються, коли ви отримуєте JSON з кількома рівнями вкладеності, тоді вам доведеться трохи попотіти, щоб його розпарсити. Тут я хотів би поділитися своїм досвідом парсингу JSON з кількома рівнями вкладеності в CoreData. Я не описуватиму, як зробити запит для отримання даних з сервера, я просто використаю JSON, який зберігається на диску, і який ми будемо парсити та записувати в CoreData.

Досвід парсингу JSON

Отже, у нас є невеликий JSON, який має певні об'єкти, що відображаються під ключем result. Ці об'єкти, у свою чергу, мають масив друзів, які також повинні бути відображені.

{"studios":["Marvel",
        "Warner Brothers",
        "DC"],
    "results":[{"name":"Daffy Duck",
        "id":548001360,
        "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ7leBgakzSpfs50MeDnkd39Lu_nZpyHIE_0tm3FzovjaGhiF7K",
        "friends":[{"name":"Bugs Bunny",
            "id":825382838,
            "avatarPath":"https://avatarfiles.alphacoders.com/812/81220.webp"},
        {"name":"Porky Pig",
            "id":819263082,
            "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTEQcXKKj5YfDTIkNPcBBNyLrCv7M5V4LiwFh1n8VJy6H5RJrBKew"}]},
    {"name":"Porky Pig",
        "id":819263082,
        "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTEQcXKKj5YfDTIkNPcBBNyLrCv7M5V4LiwFh1n8VJy6H5RJrBKew",
        "friends":[{"name":"Bugs Bunny",
            "id":825382838,
            "avatarPath":"https://avatarfiles.alphacoders.com/812/81220.webp"},
        {"name":"Daffy Duck",
            "id":548001360,
            "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ7leBgakzSpfs50MeDnkd39Lu_nZpyHIE_0tm3FzovjaGhiF7K"}]},
    {"name":"Bugs Bunny",
        "id":825382838,
        "avatarPath":"https://avatarfiles.alphacoders.com/812/81220.webp",
        "friends":[{"name":"Porky Pig",
            "id":819263082,
            "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTEQcXKKj5YfDTIkNPcBBNyLrCv7M5V4LiwFh1n8VJy6H5RJrBKew"},
        {"name":"Daffy Duck",
            "id":548001360,
            "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ7leBgakzSpfs50MeDnkd39Lu_nZpyHIE_0tm3FzovjaGhiF7K"}]}]}

Отже, щоб все правильно розпарсити і без проблем, я знайшов таку структуру в мережі, яка здатна отримувати необхідні дані з контейнера за вказаним ключем.

struct DecodingHelper {
 
    /// Динамічний ключ
    private struct Key: CodingKey {
        let stringValue: String
        init?(stringValue: String){
            self.stringValue = stringValue
            self.intValue =nil}
 
        let intValue: Int?
        init?(intValue: Int){returnnil}}
 
    /// Пустий модель, яка обробляє логіку витягування моделі з ключа
    private struct ContainerResponse<nestedmodel: decodable>: Decodable {
        let nested: NestedModel
 
        public init(from decoder: Decoder) throws {
            let key = Key(stringValue: decoder.userInfo[CodingUserInfoKey(rawValue:"key")!]! as! String)!
            let values = try decoder.container(keyedBy: Key.self)
            nested = try values.decode(NestedModel.self, forKey: key)}}
 
    static func decode<t: decodable>(modelType: T.Type, fromKey key: String, data: Data) throws -> T {
        let decoder = JSONDecoder()
 
        // ***Передаємо наш ключ через `userInfo`
        decoder.userInfo[CodingUserInfoKey(rawValue:"key")!]= key
        let model = try decoder.decode(ContainerResponse<t>.self, from: data).nested
        return model
    }}
</t></t:></nestedmodel:>

Досить зручно працювати з такою структурою.

let craracterValues = try DecodingHelper.decode(modelType:[Character].self, fromKey:"results", data: jsonData)

У нас є статичний метод, який дозволяє мені зробити такий запит і встановити всі параметри: тип моделі, яку я хочу повернути, ключ, під яким знаходиться модель, і, очевидно, самі дані, які мені потрібні. Давайте подивимося на JSON, який у нас є. Кожен рівень вкладеності має бути представленим як окремий контейнер з даними, що відповідають певному ключу.

У контейнері можуть зберігатися будь-які дані, масиви з словниками, наприклад, а також єдине значення даних, як-от Int.

Наприклад, щоб витягнути друзів в такому об'єкті:

{"name":"Porky Pig",
        "id":819263082,
        "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTEQcXKKj5YfDTIkNPcBBNyLrCv7M5V4LiwFh1n8VJy6H5RJrBKew",
        "friends":[{"name":"Bugs Bunny",
            "id":825382838,
            "avatarPath":"https://avatarfiles.alphacoders.com/812/81220.webp"},
        {"name":"Daffy Duck",
            "id":548001360,
            "avatarPath":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ7leBgakzSpfs50MeDnkd39Lu_nZpyHIE_0tm3FzovjaGhiF7K"}]}

Нам потрібно взяти

  let friendsContainer = try? generalContainer.nestedUnkeyedContainer(forKey: .friends)

контейнер за ключем .friends з основного контейнера.

Насправді, наш новий контейнер буде масивом об'єктів, і для витягування об'єктів нам потрібно пройти через нього.

if var friendsContainer = friendsContainer {
            var friends:[Character]=[]
 
            while!friendsContainer.isAtEnd {if let friend = try friendsContainer.decodeIfPresent(Character.self){
                    friends.append(friend)}}
 
            self.friends =NSSet(array: friends)}

friendsContainer має наступну властивість: isAtEnd. Отже, ми насправді ітеруємо та створюємо об'єкти, поки ця властивість не поверне "true", що означатиме кінець нашого масиву.

Висновок

Протокол Decodable — це надзвичайно потужний інструмент, який нам безкоштовно надає Apple. Дякуємо йому! Єдине, що вам потрібно зробити — це вивчити його, інакше ви не зможете розбирати певні багаторівневі JSON об'єкти.

Шановні колеги, бажаю вам удачі і сподіваюся, що стаття була корисною для вас.