Decode all properties in custom init (enumerate over all properties of a class and assign them a value)

混江龙づ霸主 提交于 2019-12-08 10:19:46

问题


I'm currently working on a project that it's API isn't ready yet. So sometimes type of some properties changes. For example I have this struct:

struct Animal: Codable {
    var tag: Int?
    var name: String?
    var type: String?
    var birthday: Date?
}

for this json:

{
    "tag": 12,
    "name": "Dog",
    "type": "TYPE1"
}

But in development, the json changes to something like this:

{
    "tag": "ANIMAL",
    "name": "Dog",
    "type": 1
}

So I get some type mismatch error in decoder and nil object. To prevent decoder from failing entire object, I implement a custom init and set nil for any unknown property and it works like charm (NOTE: I'll handle these changes later and this is only for unplanned and temporary changes):

#if DEBUG
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    tag = (try? container.decodeIfPresent(Int.self, forKey: .tag)) ?? nil
    name = (try? container.decodeIfPresent(String.self, forKey: .name)) ?? nil
    type = (try? container.decodeIfPresent(String.self, forKey: .type)) ?? nil
    birthday = (try? container.decodeIfPresent(Date.self, forKey: .birthday)) ?? nil
}
#endif

But for larger classes and struct, I have to write any property manually and it takes time and more importantly, sometimes I missed a property to set!

So is there any way to enumerate over all properties and set them? I know I can get all keys from container but don't know how to set it's corresponding property:

for key in container.allKeys {
   self.<#corresponding_property#> = (try? container.decodeIfPresent(<#corresponding_type#>.self, forKey: key)) ?? nil
}

Thanks


回答1:


The problem in your particular example is that the type of your values keeps changing: sometimes tag is a string, sometimes it's an integer. You're going to need a lot more than your Optional approach; that deals with whether something is present, not whether it has the right type. You'll need a union type that can decode and represent a string or an integer, like this:

enum Sint : Decodable {
    case string(String)
    case int(Int)
    enum Err : Error { case oops }
    init(from decoder: Decoder) throws {
        let con = try decoder.singleValueContainer()
        if let s = try? con.decode(String.self) {
            self = .string(s)
            return
        }
        if let i = try? con.decode(Int.self) {
            self = .int(i)
            return
        }
        throw Err.oops
    }
}

Using that, I was able to decode both your examples using a single Animal struct type:

struct Animal: Decodable {
    var tag: Sint
    var name: String
    var type: Sint
}
let j1 = """
{
    "tag": 12,
    "name": "Dog",
    "type": "TYPE1"
}
"""
let j2 = """
{
    "tag": "ANIMAL",
    "name": "Dog",
    "type": 1
}
"""
let d1 = j1.data(using: .utf8)!
let a1 = try! JSONDecoder().decode(Animal.self, from: d1)
let d2 = j2.data(using: .utf8)!
let a2 = try! JSONDecoder().decode(Animal.self, from: d2)

Okay, but now let's say you don't even know what the keys are going to be. Then you need an AnyCodingKey type that can mop up the keys no matter what they are, and instead of multiple properties, your Animal will have a single property that's a Dictionary, like this:

struct Animal: Decodable {
    var d = [String : Sint]()
    struct AnyCodingKey : CodingKey {
        var stringValue: String
        var intValue: Int?

        init(_ codingKey: CodingKey) {
            self.stringValue = codingKey.stringValue
            self.intValue = codingKey.intValue
        }
        init(stringValue: String) {
            self.stringValue = stringValue
            self.intValue = nil
        }
        init(intValue: Int) {
            self.stringValue = String(intValue)
            self.intValue = intValue
        }
    }
    init(from decoder: Decoder) throws {
        let con = try decoder.container(keyedBy: AnyCodingKey.self)
        for key in con.allKeys {
            let result = try con.decode(Sint.self, forKey: key)
            self.d[key.stringValue] = result
        }
    }
}

So now you can decode something with complete unknown keys whose value can be string or integer. Again, this works fine against the JSON examples you gave.

Note that this is the inverse of what you originally asked to do. Instead of using the struct property names to generate the keys, I've simply accepted any key of either type and stored it flexibly in the struct through the use of the dictionary. You could also put a property façade in front of that dictionary using the new Swift 4.2 dynamicMemberLookup feature. But that is left as an exercise for the reader!




回答2:


The tool you want is Sourcery. It's a meta-programming wrapper for Swift that will write the boilerplate for you, since you know what you want to write, it's just tedious (that's what Sourcery is ideal for). The important thing about Sourcery is (despite the name), there's no magic. It just writes Swift code for you based on other Swift code. That makes it easy to pull out later when you don't need it anymore.



来源:https://stackoverflow.com/questions/52818996/decode-all-properties-in-custom-init-enumerate-over-all-properties-of-a-class-a

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