Swift:挂接到 Decodable.init() 以获取未指定的密钥?

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

我有一些 JSON 我想用 JSONDecoder 解码。问题是,其中一个属性的名称在从服务器发送时是动态的。

像这样:

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

如果 someRandomName 在构建时未知,我该如何解码?我一直在浏览 www 寻找答案,但仍然没有快乐。无法真正理解 DecodableCodingKey 的工作原理。有的例子长达几十行,好像不太对!

编辑 我应该指出密钥在运行时是已知的,所以也许我可以在解码对象时传入它?

是否有任何方法可以连接到协议方法或属性之一以启用此解码?我不介意是否必须为此对象编写定制解码器:所有其他 JSON 都很好且标准。

编辑

好的,我的理解已经到此为止了:

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

所以我现在明白 CodingKey 一致性正在为序列化数据生成密钥,而不是 Swift 结构(现在我想起来这很有道理)。

那么我现在如何即时生成 pair 的案例,而不是像这样硬编码呢?我知道它与我需要实现的 init(from decoder: Decoder) 有关,但对于我来说,我无法弄清楚它是如何工作的。请帮忙!

编辑 2

好的,我现在很接近了。解码似乎适用于此:

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

这段代码似乎完成了它的工作:检查函数本身的 lastpair 属性,看起来不错;但我在尝试解码时遇到错误:

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

我离得很近我能尝到它的味道...

你要找的是 JSONSerializer 而不是 JSONDecoder 我猜是 https://developer.apple.com/documentation/foundation/jsonserialization.

因为密钥不可预测,所以最好转换成Dictionary。或者你可以看看这个https://swiftsenpai.com/swift/decode-dynamic-keys-json/

如果动态密钥在运行时已知,您可以通过解码器的 userInfo 字典传递它。

首先创建两个扩展

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

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

在结构中实现 CodingKeys 作为能够动态创建键的结构。

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

现在创建 JSONDecoder 传递已知的动态名称

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

编辑:

如果 JSON 始终只包含两个键,这是一种更简单的方法:

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

我现在有了一些实际有效的代码!

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: { [=10=].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)
}