iUrii
iUrii

Reputation: 13768

Swift decodable with programatically provided coding keys

This is a simplified model that is decoded from JSON:

struct Info: Decodable {
    var text: String
    var num: Int
}

struct Root: Decodable {
    let info: Info
}

Sometimes I need to decode Info.text or Info.num only but sometimes both of them and to support all options I've made similar structs for decoding e.g:

// For text only
struct InfoText: Decodable {
    var text: String
}

struct RootText: Decodable {
    let info: InfoText
}

// For num only
struct InfoNum: Decodable {
    var num: Int
}

struct RootNum: Decodable {
    let info: InfoNum
}

This approach produces much cloned code and runtime checks to process the structs so is it possible to decode provided coding keys only with the single struct?

Upvotes: 1

Views: 335

Answers (1)

iUrii
iUrii

Reputation: 13768

It's possible to provide any contextual information to the decoder with userInfo property and in this case we can pass an array of coding keys and use this info in the decoding process:

struct Info: Decodable {
    var text: String?
    var num: Int?
    
    static var keys = CodingUserInfoKey(rawValue: "keys")!
    
    enum CodingKeys: String, CodingKey {
        case text, num
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        guard let keys = decoder.userInfo[Self.keys] as? [CodingKeys] else {
            return
        }
        
        if keys.contains(.text) {
            text = try container.decode(String.self, forKey: .text)
        }
        
        if keys.contains(.num) {
            num = try container.decode(Int.self, forKey: .num)
        }
    }
}


struct Root: Decodable {
    let info: Info
}

let json = #"{ "info" : { "text": "Hello", "num": 20 } }"#.data(using: .utf8)!

let decoder = JSONDecoder()
let keys: [Info.CodingKeys] = [.text]
decoder.userInfo[Info.keys] = keys
let root = try decoder.decode(Root.self, from: json)
print(root)

// Outputs:
Root(info: Info(text: Optional("Hello"), num: nil))

Upvotes: 2

Related Questions