Удобный способ декодирования вложенного объекта JSON в Swift?

Допустим, у вас есть JSON:

{
    "status": "error",
    "data": {
        "errormessage": "Could not get user with ID: -1.",
        "errorcode": 14
    }
}

Для заданной структуры Error:

struct APIError: Decodable {
    let code: Int?
    let message: String?

    enum CodingKeys: String, CodingKey {
        case code = "errorcode"
        case message = "errormessage"
    }
}

Нажмите на веб-сервис, получите JSON и инициализируйте структуру:

let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest)
{ (data, response, error) in
    // Doesn't work because the portion of the JSON we want is in the "data" key
    let e = try? JSONDecoder().decode(APIError.self, from: data)
}
task.resume()

Есть ли простой способ сделать что-то вроде data["data"]? Какой правильной модели следовать?

Решение А. Преобразуйте данные в объект JSON, получите нужный объект, затем преобразуйте его в объект данных и выполните декодирование.

let jsonFull = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
let json = jsonFull["data"]
let data_error = try? JSONSerialization.data(withJSONObject: json, options: [])
let e = try? JSONDecoder().decode(APIError.self, from: data_error)

Решение Б. Оберните целевой элемент в другую структуру

struct temp : Decodable {
    let status: String?
    let data: APIError?
}

let e = try? JSONDecoder().decode(temp.self, from: data).data

Решение C. Установите вложенную структуру в декодировании (что, если это несколько объектов в глубину?)

let e = try? JSONDecoder().decode([Any, APIError.self], from: data)

Какие шаблоны мне не хватает? Какой самый элегантный способ сделать это?


person GoldenJoe    schedule 24.01.2018    source источник
comment
Решение C не работает (Any не поддерживается). Существует решение D с использованием nestedContainers, но в этом случае решение B является обычным.   -  person vadian    schedule 24.01.2018


Ответы (1)


Вы можете использовать следующий подход:

struct APIError: Decodable {
    let code: Int
    let message: String

    enum CodingKeys: String, CodingKey {
        case data
    }

    enum ErrorCodingKeys: String, CodingKey {
        case code = "errorcode"
        case message = "errormessage"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let nestedContainer = try container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .data)

        code = try nestedContainer.decode(Int.self, forKey: .code)
        message = try nestedContainer.decode(String.self, forKey: .message)
    }
}

let data = try! JSONSerialization.data(withJSONObject: ["status": "error", "data": ["errorcode": 14, "errormessage": "Could not get user with ID: -1."]], options: [])
let error = try! JSONDecoder().decode(APIError.self, from: data)
person Alexander Gaidukov    schedule 24.01.2018
comment
Это кажется действительно чувствительным. Что произойдет, если вы захотите расшифровать ошибку APIError в другой части API? Например, может быть API запутанный, а в некоторых конечных точках вместо этого ["status": "error", "body": ["errorcode": 14, "errormessage": "Could not get user with ID: -1."]]. - person GoldenJoe; 24.01.2018
comment
Решение B в этом случае также не сработает. А если в другой части API это будут не errorcode и errormessage, а error и message? Таким образом, в этом случае подходит только ручной разбор (решение А). - person Alexander Gaidukov; 24.01.2018
comment
Я думал о написании расширения для удобного захвата ключевых путей из данных. Есть ли причины, по которым это не популярное решение? Почему бы Apple не предоставить более простой способ работы с ключевыми путями? - person GoldenJoe; 24.01.2018
comment
Я когда-то написал такое расширение. Вы можете увидеть это здесь: swift.sandbox.bluemix.net/#/repl/5a68cc9a4b3a155f556c998d< /а> - person Alexander Gaidukov; 24.01.2018