Home > database >  Swift: hook into Decodable.init() for an unspecified key?
Swift: hook into Decodable.init() for an unspecified key?

Time:03-19

I have some JSON I would like to decode with a JSONDecoder. Trouble is, the name of one of the properties is helpfully dynamic when sent from the server.

Like this:

{
   "someRandomName": [ [1,2,3], [4,5,6] ],
   "staticName": 12345
}

How can I decode this, when the someRandomName is not known at build time? I have been trawling through the www looking for an answer, but still no joy. Can't really get my head around how this Decodable, CodingKey stuff works. Some of the examples are dozens of lines long, and that doesn't seem right!

EDIT I should point out that the key is known at runtime, so perhaps I can pass it in when decoding the object?

Is there any way to hook into one of the protocol methods or properties to enable this decoding? I don't mind if I have to write a bespoke decoder for just this object: all the other JSON is fine and standard.

EDIT

Ok, my understanding has taken me this far:

struct Pair: Decodable {
    var pair: [[Double]]
    var last: Int
    
    private struct CodingKeys: CodingKey {
        
        var stringValue: String
        
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        // Use for integer-keyed dictionary
        var intValue: Int?
        init?(intValue: Int) {
            // We are not using this, thus just return nil
            return nil
        }
    }
    
    init(from decoder: Decoder) throws {
        
        // just to stop the compiler moaning
        pair = [[]]
        last = 0
        
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // how do I generate the key for the correspond "pair" property here?
        for key in container.allKeys {
            last = try container.decode(Int.self, forKey: CodingKeys(stringValue: "last")!)
            pair = try container.decode([[Double]].self, forKey: CodingKeys(stringValue: key.stringValue)!)
        }
        
    }
    
}

init() {
    let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
    
    let jsonData = Data(jsonString.utf8)
    
    // this gives: "Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead.", underlyingError: nil))"
    let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)

    dump(decodedResult)
}

So I now understand that the CodingKey conformance is generating the keys for the serialized data, not the Swift struct (which kinda makes perfect sense now I think about it).

So how do I now generate the case for pair on the fly, rather than hard-coding it like this? I know it has something to do with the init(from decoder: Decoder) I need to implement, but for the life of me I can't work out how that functions. Please help!

EDIT 2

Ok, I'm so close now. The decoding seems to be working with this:

struct Pair: Decodable {
    var pair: [[Double]]
    var last: Int
    
    private enum CodingKeys : String, CodingKey {
        case last
    }
    
    private struct DynamicCodingKeys: CodingKey {
        var stringValue: String
        
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        // Use for integer-keyed dictionary
        var intValue: Int?
        init?(intValue: Int) {
            // We are not using this, thus just return nil
            return nil
        }
    }
    
    init(from decoder: Decoder) throws {
        
        // just to stop the compiler moaning
        pair = [[]]
        last = 0
        
        let container1 = try decoder.container(keyedBy: CodingKeys.self)
        last = try container1.decode(Int.self, forKey: .last)
        
        let container2 = try decoder.container(keyedBy: DynamicCodingKeys.self)
        for key in container2.allKeys {
            pair = try container2.decode([[Double]].self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
        }
        
    }
    
}

This code seems to do its job: examining the last and pair properties in the function itself and it looks good; but I'm getting an error when trying to decode:

init() {
    let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
    
    let jsonData = Data(jsonString.utf8)
    
    // Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [DynamicCodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead."
    let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
    
    dump(decodedResult)
}

I'm so close I can taste it...

CodePudding user response:

If the dynamic key is known at runtime, you can pass it via the userInfo dictionary of the decoder.

First of all create two extensions

extension CodingUserInfoKey {
    static let dynamicKey = CodingUserInfoKey(rawValue: "dynamicKey")!
}

extension JSONDecoder {
    convenience init(dynamicKey: String) {
        self.init()
        self.userInfo[.dynamicKey] = dynamicKey
    }
}

In the struct implement CodingKeys as struct to be able to create keys on the fly.

struct Pair : Decodable {
    let last : Int
    let pair : [[Double]]
    
    private struct CodingKeys: CodingKey {
        var intValue: Int?
        var stringValue: String
        init?(stringValue: String) {  self.stringValue = stringValue  }
        init?(intValue: Int) {
            self.stringValue = String(intValue)
            self.intValue = intValue
        }
        static let last = CodingKeys(stringValue: "last")!
        static func makeKey(name: String) -> CodingKeys {
            return CodingKeys(stringValue: name)!
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        guard let dynamicKey = decoder.userInfo[.dynamicKey] as? String else {
            throw DecodingError.dataCorruptedError(forKey: .makeKey(name: "pair"), in: container, debugDescription: "Dynamic key in userInfo is missing")
        }
        last = try container.decode(Int.self, forKey: .last)
        pair = try container.decode([[Double]].self, forKey: .makeKey(name: dynamicKey))
    }
}

Now create the JSONDecoder passing the known dynamic name

let jsonString = """
{
  "last": 123456,
  "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""

do {
    let decoder = JSONDecoder(dynamicKey: "XBTUSD")
    let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8))
    print(result)
} catch {
    print(error)
}

Edit:

If the JSON contains always only two keys this is an easier approach:

struct AnyKey: CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init?(stringValue: String) {  self.stringValue = stringValue  }
    init?(intValue: Int) {
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}

struct Pair : Decodable {
    let last : Int
    let pair : [[Double]]
}

let jsonString = """
{
  "last": 123456,
  "XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .custom({ codingPath in
        let lastPath = codingPath.last!
        if lastPath.stringValue == "last" { return lastPath }
        return AnyKey(stringValue: "pair")!
    })
    let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8))
    print(result)
} catch {
    print(error)
}

CodePudding user response:

You're looking for JSONSerializer not JSONDecoder I guess, https://developer.apple.com/documentation/foundation/jsonserialization.

Because the key is unpredictable, so better convert to Dictionary. Or you can take a look at this https://swiftsenpai.com/swift/decode-dynamic-keys-json/

CodePudding user response:

I now have some code that actually works!

struct Pair: Decodable {
    var pair: [[Double]]
    var last: Int
    
    private struct CodingKeys: CodingKey {
        var intValue: Int?
        var stringValue: String
        init?(stringValue: String) {  self.stringValue = stringValue  }
        init?(intValue: Int) {
            self.stringValue = String(intValue)
            self.intValue = intValue
        }
        static let last = CodingKeys(stringValue: "last")!
        static func makeKey(name: String) -> CodingKeys {
            return CodingKeys(stringValue: name)!
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        last = try container.decode(Int.self, forKey: .last)
        let key = container.allKeys.first(where: { $0.stringValue != "last" } )?.stringValue
        pair = try container.decode([[Double]].self, forKey: .makeKey(name: key!))
    }
    
}
     
init() {
    let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
    
    let jsonData = Data(jsonString.utf8)
    
    // Ask JSONDecoder to decode the JSON data as DecodedArray
    let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
    
    dump(decodedResult)
}
  • Related