Найдите идеальный JSON-анализатор для Core Data - с возможностью кодирования от Apple

Доброго всем времени суток уважаемые коллеги iOS-ники, наверняка каждый из вас работал с сетью и занимался парсингом данных c JSON. Для этого процесса есть куча библиотек, всевозможных инструментов которые можно юзать. Некоторые из них сложные, а некоторые простые. Я и сам очень долго если чесно парсил JSON руками, не доверяя этот процес каким-то сторонним библиотекам и в этом были свои плюсы.

Правда однажды я для себя открыл протокол Decodable который умеет парсить данные за нас, и честно говоря по началу это была магия для меня, особенно если с сервера приходит JSON без большого уровня вложенности.

Если иметь какие-то элементарные знания про этот протокол то можно распарсить не сложный JSON за одну минутку. И, да,на многих ресурсах есть куча примеров как это сделать. Вопрос возникает как только ты получаешь JSON с несколькими уровнями вложенности, тогда и начинаются пляски вокруг костра.

В этой статье хочу рассказать вам как раз о своем опыте, как парсить JSON c несколькими уровнями вложенности в coreData.

Я не буду описывать здесь как делать риквест чтобы получить данные с сервера, а просто буду использовать JSON который будет храниться на диске и который мы и будем собственно парсить и записывать в coreDate. Да начнется магия!

У нас есть небольшой 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.jpg"
        },
        {
            "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.jpg"
        },
        {
            "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.jpg",
        "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 {
 
    /// Dynamic key
    private struct Key: CodingKey {
        let stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
            self.intValue = nil
        }
 
        let intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }
 
    /// Dummy model that handles model extracting logic from a key
    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()
 
        // ***Pass in our key through `userInfo`
        decoder.userInfo[CodingUserInfoKey(rawValue: "key")!] = key
        let model = try decoder.decode(ContainerResponse<T>.self, from: data).nested
        return model
    }
}

Работать с этой структурой достаточно удобно:

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

У нас есть статический метод благодаря которому я просто могу сделать такой вызов и передать все параметры: тип модели которую мне нужно будет вернуть, ключ по которому эта модель лежит ну и конечно же сами данные. Если смотреть на JSON который у нас есть, каждый уровень вложенности нужно представлять как отдельный контейнер с данными по какому-то ключу.

В контейнере могут лежать какие угодно данные, например массив с дикшенарями, либо одно простое значения такое как Int.

И чтобы например распарсить нам friends в таком вот объекте:

    {
        "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.jpg"
        },
        {
            "name": "Daffy Duck",
            "id": 548001360,
            "avatarPath": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ7leBgakzSpfs50MeDnkd39Lu_nZpyHIE_0tm3FzovjaGhiF7K"
        }
        ]
    }

Нам нужно взять

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

С главного контейнера child контейнер по ключу .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 protocol — очень мощный инструмент который благодаря Apple нам дается бесплатно из коробки, бери и пользуйся. Единственное что нужно, так это в нем разобраться, ибо без этого вы попросту не сможете парсить какие-то многоуровневые JSON объекты.

Дорогие коллеги желаю вам всем удачи и надеюсь что эта статья была для вас полезной.