Using Decodable protocol with multiples keys

被刻印的时光 ゝ 提交于 2019-12-06 16:00:24

My favorite approach when it comes to decoding nested JSON data is to define a "raw" model that stays very close to the JSON, even using snake_case if needed. It help bringing JSON data into Swift really quickly, then you can use Swift to do the manipulations you need:

struct Person: Decodable {
    let firstName, lastName: String
    let age: String?

    // This matches the keys in the JSON so we don't have to write custom CodingKeys    
    private struct RawPerson: Decodable {
        struct RawAge: Decodable {
            let realage: String?
            let fakeage: String?
        }

        let firstname: String
        let lastname: String
        let age: RawAge
    }

    init(from decoder: Decoder) throws {
        let rawPerson  = try RawPerson(from: decoder)
        self.firstName = rawPerson.firstname
        self.lastName  = rawPerson.lastname
        self.age       = rawPerson.age.realage
    }
}

Also, I recommend you to be judicious with the use of Codable, as it implies both Encodable and Decodable. It seems like you only need Decodable so conform your model to that protocol only.

For greater flexibility and robustness, you could implement an Age enumeration to fully support your data model head-on ;) For instance:

enum Age: Decodable {
    case realAge(String)
    case fakeAge(String)

    private enum CodingKeys: String, CodingKey {
        case realAge = "realage", fakeAge = "fakeage"
    }

    init(from decoder: Decoder) throws {
        let dict = try decoder.container(keyedBy: CodingKeys.self)
        if let age = try dict.decodeIfPresent(String.self, forKey: .realAge) {
            self = .realAge(age)
            return
        }
        if let age = try dict.decodeIfPresent(String.self, forKey: .fakeAge) {
            self = .fakeAge(age)
            return
        }
        let errorContext = DecodingError.Context(
            codingPath: dict.codingPath,
            debugDescription: "Age decoding failed"
        )
        throw DecodingError.keyNotFound(CodingKeys.realAge, errorContext)
    }
}

and then use it in your Person type:

struct Person: Decodable {
    let firstName, lastName: String
    let age: Age

    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    }

    var realAge: String? {
        switch age {
        case .realAge(let age): return age
        case .fakeAge: return nil
        }
    }
}

Decode as before:

let jsonData = """
[
    {"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
    {"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!

let decoded = try! JSONDecoder().decode([Person].self, from: jsonData)
for person in decoded { print(person) }

prints:

Person(firstName: "Tom", lastName: "Smith", age: Age.realAge("28"))
Person(firstName: "Bob", lastName: "Smith", age: Age.fakeAge("31"))


Finally, the new realAge computed property provides the behavior you were after initially (i.e., non-nil only for real ages):

for person in decoded { print(person.firstName, person.realAge) }

Tom Optional("28")
Bob nil

There are times to trick the API to get the interface you want.

let jsonData = """
[
    {"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
    {"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!

struct Person: Codable {
    let firstName: String
    let lastName: String
    var age: String? { return _age["realage"] }

    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case _age = "age"
    }

    private let _age: [String: String]
}

do {
    let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
    print(decoded)

    let encoded = try JSONEncoder().encode(decoded)
    if let encoded = String(data: encoded, encoding: .utf8) { print(encoded) }
} catch {
    print(error)
}

Here the API (firstName, lastName, age) is kept and the JSON is preserved in both directions.

You can use like this :

struct Person: Decodable {
    let firstName, lastName: String
    var age: Age?

    enum CodingKeys: String, CodingKey {
        case firstName = "firstname"
        case lastName = "lastname"
        case age
    }
}

struct Age: Decodable {
    let realage: String?
}

You can call like this :

do {
    let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
    print(decoded[0].age?.realage) // Optional("28")
    print(decoded[1].age?.realage) // nil
} catch {
    print("error")
}

Lots of great answers here. I have certain reasons for not wanting to make it into it's own data model. Specially in my case it comes with a lot of data I don't need and this specific thing I need corresponds more to a person than an age model.

I'm sure others will find this post useful tho which is amazing. Just to add to that I will post my solution for how I decided to do this.

After looking at the Encoding and Decoding Custom Types Apple Documentation, I found it was possible to build a custom decoder and encoder to achieve this (Encode and Decode Manually).

struct Coordinate: Codable {
    var latitude: Double
    var longitude: Double
    var elevation: Double

    enum CodingKeys: String, CodingKey {
        case latitude
        case longitude
        case additionalInfo
    }

    enum AdditionalInfoKeys: String, CodingKey {
        case elevation
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)

        let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)

        var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        try additionalInfo.encode(elevation, forKey: .elevation)
    }

}

The one change that is included in the code above that Apple doesn't mention is the fact that you can't use extensions like in their documentation example. So you have to embed it right within the struct or class.

Hopefully this helps someone, along with the other amazing answers here.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!