Изменение типа ключа в словаре JSON - PullRequest
1 голос
/ 25 мая 2020

Я столкнулся с ответом JSON, который не похож ни на один из тех, с которыми я привык работать. Вместо пар ключ-значение в строке есть раздел, в котором есть словарь с String ключами и Int значениями. Я хотел бы декодировать строковый ключ из словаря как объект Date.

Вот рассматриваемый JSON:

[
    {
        "country": "Afghanistan",
        "province": null,
        "timeline": {
            "cases": {
                "5/22/20": 9216,
                "5/23/20": 9998,
                "5/24/20": 10582
            },
            "deaths": {
                "5/22/20": 205,
                "5/23/20": 216,
                "5/24/20": 218
            },
            "recovered": {
                "5/22/20": 996,
                "5/23/20": 1040,
                "5/24/20": 1075
            }
        }
    },
]

Вот что я написал для Codable Struct to декодировать его:

struct HistoricCountry: Codable {
    let country: String
    let province: String?
    let timeline: Timeline
}

struct Timeline: Codable {
    let cases, deaths, recovered: [String: Int]
}

Вот код, который я написал для его декодирования. Это сработает, если я оставлю Timeline в качестве словаря с ключом в виде пары ключ-значение строка / int, JSON декодируется правильно:

do {
    let decoder = JSONDecoder()
    let formatter = DateFormatter()
    formatter.dateFormat = "M/dd/yy"
    decoder.dateDecodingStrategy = .formatted(formatter)

    let decodedElements = try decoder.decode([HistoricCountry].self, from: data)
    XCTAssertEqual(decodedElements.count, 266)
} catch {
    XCTFail("\n??? Decoding failed ???:\n\(error)")
}

Если я изменю пары значений ключа Timeline на [Date: Int]:

struct Timeline: Codable {
    let cases, deaths, recovered: [Date: Int]
}

Я получаю эту ошибку:

??? Decoding failed ???:
typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "timeline", intValue: nil), CodingKeys(stringValue: "cases", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

Что я делаю не так и как исправить?

Ответы [ 2 ]

1 голос
/ 25 мая 2020

Codable поддерживает только Strings и Ints в качестве ключей для контейнеров с ключами , поэтому использовать Date в качестве ключа невозможно. (Если вы попытаетесь, он попытается декодировать массив формата [key1, value1, key2, value2, ...], что объясняет полученную вами ошибку).

Что вы можете сделать, так это использовать оболочку свойств, которая кодирует и декодирует как [String:Int] и преобразует строки в даты и обратно:

import Foundation

let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "M/dd/yy"
    return formatter
}()

@propertyWrapper
struct DateKeyed<T> {
    var wrappedValue: [Date: T]
}

extension DateKeyed: Encodable where T: Encodable {
    enum EncodingError: Error {
        case duplicateKey
    }
    func encode(to encoder: Encoder) throws {
        try Dictionary(
            wrappedValue.map { (dateFormatter.string(from: $0.key), $0.value) },
            uniquingKeysWith: { _, _ in throw EncodingError.duplicateKey }
        ).encode(to: encoder)
    }
}

extension DateKeyed: Decodable where T: Decodable {
    enum DecodingError: Error {
        case duplicateKey
        case invalidDate(String)
    }

    init(from decoder: Decoder) throws {
        let dict = try [String:T](from: decoder)
        try self.init(wrappedValue: Dictionary(
            dict.map {
                guard let date = dateFormatter.date(from: $0.key) else { throw DecodingError.invalidDate($0.key) }
                return (date, $0.value)
            },
            uniquingKeysWith: { _, _ in throw DecodingError.duplicateKey }
        ))
    }
}

struct Timeline: Codable {
    @DateKeyed var cases: [Date:Int]
}

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
try print(String(data: encoder.encode(Timeline(cases: [Date(): 5, Date(timeIntervalSinceReferenceDate: 0): 10])), encoding: .utf8)!)

Вывод:

{
  "cases" : {
    "5\/25\/20" : 5,
    "12\/31\/00" : 10
  }
}
1 голос
/ 25 мая 2020

Вы не можете декодировать JSON ключи с помощью стратегии декодирования.

Следующий код, безусловно, может быть оптимизирован, но он отображает строковые ключи на Date

struct Timeline: Codable {

    let formatter : DateFormatter = {
        let fm = DateFormatter()
        fm.locale = Locale(identifier: "en_US_POSIX")
        fm.dateFormat = "M/dd/yy"
        return fm
    }()

    private enum CodingKeys : String, CodingKey { case cases, deaths, recovered }
    let cases, deaths, recovered: [Date: Int]

    init(from decoder : Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let casesData = try container.decode([String: Int].self, forKey: .cases)
        var caseResult = [Date:Int]()
        for (key, value) in casesData {
            let date = formatter.date(from: key)!
            caseResult[date] = value
        }
        cases = caseResult

        let deathsData = try container.decode([String: Int].self, forKey: .deaths)
        var deathsResult = [Date:Int]()
        for (key, value) in deathsData {
            let date = formatter.date(from: key)!
            deathsResult[date] = value
        }
        deaths = deathsResult

        let recoveredData = try container.decode([String: Int].self, forKey: .recovered)
        var recoveredResult = [Date:Int]()
        for (key, value) in recoveredData {
            let date = formatter.date(from: key)!
            recoveredResult[date] = value
        }

        recovered = recoveredResult
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...