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)
}