如何处理完全动态的 JSON 响应

Posted

技术标签:

【中文标题】如何处理完全动态的 JSON 响应【英文标题】:How to deal with completely dynamic JSON responses 【发布时间】:2018-01-30 20:04:54 【问题描述】:

也许社区中有人遇到过类似的困难,并提出了可行的解决方案。

我们目前正在开发多语言键/值存储。鉴于此,我们通常不知道将提前存储什么。

考虑以下结构

struct Character : Codable, Equatable 
    let name:    String
    let age:     Int
    let gender:  Gender
    let hobbies: [String]

    static func ==(lhs: Character, rhs: Character) -> Bool 
        return (lhs.name == rhs.name
                   && lhs.age == rhs.age
                   && lhs.gender == rhs.gender
                   && lhs.hobbies == rhs.hobbies)
    

当通过网络发送/接收字符实体时,一切都相当简单。用户可以提供我们可以解码的类型。

但是,我们确实能够动态查询存储在后端的实体。例如,我们可以请求 'name' 属性的值并将其返回。

这种活力是一个痛点。除了不知道属性的类型之外,它们是可编码的,返回的格式也可以是动态的。

以下是提取属性的两个不同调用的一些响应示例:

"value":"Bilbo"

"value":["[Ljava.lang.Object;",["Bilbo",111]]

在某些情况下,它可能相当于字典。

现在,我有以下处理响应的结构:

fileprivate struct ScalarValue<T: Decodable> : Decodable 
    var value: T?

使用 Character 示例,传递给解码器的类型将是:

ScalarValue<Character>.self

但是,对于单个值、数组或字典的情况,我有点卡住了。

我从类似的东西开始:

fileprivate struct AnyDecodable: Decodable 
    init(from decoder: Decoder) throws 
        // ???
    

根据我上面描述的可能的返回类型,我不确定当前的 API 是否可行。

想法?

【问题讨论】:

请注意Codable 的魔力(采用协议即可完成)依赖于具体类型。越动态越不方便(意味着样板代码)。还要考虑发送方设计以发送尽可能一致/同质的数据 是的,我知道可能是这种情况,但目前尚不清楚如何处理它。后端不会改变,所以需要在客户端/swift端进行解析。 您需要一些结构来响应响应,并且在Object 的情况下,您有一个值数组而不是字典,这使得运行时很难弄清楚它应该解码什么。也许如果“[Ljava.lang.Object;”还将包含一个具体的类名,这将在编写自定义解码器时为您提供帮助,否则真的很难。 在我看来,旧的 JSONSerialization 和遍历 [String: Any] 字典可能更适合这个。 这样的实现的一个限制是所有属性都必须是可选的,因为您可以请求属性集的任何子集。 【参考方案1】:

Swift 绝对可以处理任意可解码的 JSON。这与任意解码不同。 JSON 无法编码所有可能的值。但是这个结构会解码任何可以用 JSON 表达的东西,然后你可以从那里以一种类型安全的方式探索它,而无需求助于像 Any 这样的危险和笨拙的工具。

enum JSON: Decodable, CustomStringConvertible 
    var description: String 
        switch self 
        case .string(let string): return "\"\(string)\""
        case .number(let double):
            if let int = Int(exactly: double) 
                return "\(int)"
             else 
                return "\(double)"
            
        case .object(let object):
            return "\(object)"
        case .array(let array):
            return "\(array)"
        case .bool(let bool):
            return "\(bool)"
        case .null:
            return "null"
        
    

    var isEmpty: Bool 
        switch self 
        case .string(let string): return string.isEmpty
        case .object(let object): return object.isEmpty
        case .array(let array): return array.isEmpty
        case .null: return true
        case .number, .bool: return false
        
    

    struct Key: CodingKey, Hashable, CustomStringConvertible 
        var description: String 
            return stringValue
        

        var hashValue: Int  return stringValue.hash 

        static func ==(lhs: JSON.Key, rhs: JSON.Key) -> Bool 
            return lhs.stringValue == rhs.stringValue
        

        let stringValue: String
        init(_ string: String)  self.stringValue = string 
        init?(stringValue: String)  self.init(stringValue) 
        var intValue: Int?  return nil 
        init?(intValue: Int)  return nil 
    

    case string(String)
    case number(Double) // FIXME: Split Int and Double
    case object([Key: JSON])
    case array([JSON])
    case bool(Bool)
    case null

    init(from decoder: Decoder) throws 
        if let string = try? decoder.singleValueContainer().decode(String.self)  self = .string(string) 
        else if let number = try? decoder.singleValueContainer().decode(Double.self)  self = .number(number) 
        else if let object = try? decoder.container(keyedBy: Key.self) 
            var result: [Key: JSON] = [:]
            for key in object.allKeys 
                result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
            
            self = .object(result)
        
        else if var array = try? decoder.unkeyedContainer() 
            var result: [JSON] = []
            for _ in 0..<(array.count ?? 0) 
                result.append(try array.decode(JSON.self))
            
            self = .array(result)
        
        else if let bool = try? decoder.singleValueContainer().decode(Bool.self)  self = .bool(bool) 
        else 
            self = .null
        
    

    var objectValue: [String: JSON]? 
        switch self 
        case .object(let object):
            let mapped: [String: JSON] = Dictionary(uniqueKeysWithValues:
                object.map  (key, value) in (key.stringValue, value) )
            return mapped
        default: return nil
        
    

    var arrayValue: [JSON]? 
        switch self 
        case .array(let array): return array
        default: return nil
        
    

    subscript(key: String) -> JSON? 
        guard let jsonKey = Key(stringValue: key),
            case .object(let object) = self,
            let value = object[jsonKey]
            else  return nil 
        return value
    

    var stringValue: String? 
        switch self 
        case .string(let string): return string
        default: return nil
        
    

    var doubleValue: Double? 
        switch self 
        case .number(let number): return number
        default: return nil
        
    

    var intValue: Int? 
        switch self 
        case .number(let number): return Int(number)
        default: return nil
        
    

    subscript(index: Int) -> JSON? 
        switch self 
        case .array(let array): return array[index]
        default: return nil
        
    

    var boolValue: Bool? 
        switch self 
        case .bool(let bool): return bool
        default: return nil
        
    

有了这个,你可以做这样的事情:

let bilboJSON = """
"value":"Bilbo"
""".data(using: .utf8)!

let bilbo = try! JSONDecoder().decode(JSON.self, from: bilboJSON)
bilbo["value"]  // "Bilbo"

let javaJSON = """
"value":["[Ljava.lang.Object;",["Bilbo",111]]
""".data(using: .utf8)!

let java = try! JSONDecoder().decode(JSON.self, from: javaJSON)
java["value"]?[1]   // ["Bilbo", 111]
java["value"]?[1]?[0]?.stringValue  // "Bilbo" (as a String rather than a JSON.string)

? 的扩散有点难看,但是在我的实验中使用throws 并没有真正使界面变得更好(特别是因为下标不能抛出)。建议根据您的特定用例进行一些调整。

【讨论】:

【参考方案2】:

为此,我自己编写了一个 AnyCodable 结构:

struct AnyCodable: Decodable 
  var value: Any

  struct CodingKeys: CodingKey 
    var stringValue: String
    var intValue: Int?
    init?(intValue: Int) 
      self.stringValue = "\(intValue)"
      self.intValue = intValue
    
    init?(stringValue: String)  self.stringValue = stringValue 
  

  init(value: Any) 
    self.value = value
  

  init(from decoder: Decoder) throws 
    if let container = try? decoder.container(keyedBy: CodingKeys.self) 
      var result = [String: Any]()
      try container.allKeys.forEach  (key) throws in
        result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
      
      value = result
     else if var container = try? decoder.unkeyedContainer() 
      var result = [Any]()
      while !container.isAtEnd 
        result.append(try container.decode(AnyCodable.self).value)
      
      value = result
     else if let container = try? decoder.singleValueContainer() 
      if let intVal = try? container.decode(Int.self) 
        value = intVal
       else if let doubleVal = try? container.decode(Double.self) 
        value = doubleVal
       else if let boolVal = try? container.decode(Bool.self) 
        value = boolVal
       else if let stringVal = try? container.decode(String.self) 
        value = stringVal
       else 
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
      
     else 
      throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
    
  


extension AnyCodable: Encodable 
  func encode(to encoder: Encoder) throws 
    if let array = value as? [Any] 
      var container = encoder.unkeyedContainer()
      for value in array 
        let decodable = AnyCodable(value: value)
        try container.encode(decodable)
      
     else if let dictionary = value as? [String: Any] 
      var container = encoder.container(keyedBy: CodingKeys.self)
      for (key, value) in dictionary 
        let codingKey = CodingKeys(stringValue: key)!
        let decodable = AnyCodable(value: value)
        try container.encode(decodable, forKey: codingKey)
      
     else 
      var container = encoder.singleValueContainer()
      if let intVal = value as? Int 
        try container.encode(intVal)
       else if let doubleVal = value as? Double 
        try container.encode(doubleVal)
       else if let boolVal = value as? Bool 
        try container.encode(boolVal)
       else if let stringVal = value as? String 
        try container.encode(stringVal)
       else 
        throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
      

    
  

它也适用于嵌套字典/数组。您可以在操场上使用任何 json 进行尝试。

let decoded = try! JSONDecoder().decode(AnyCodable.self, from: jsonData)

【讨论】:

【参考方案3】:

是的,可以通过现有的Codable API 实现您所描述的内容,并且我会以一种优雅的方式说(尽管我在这里可能是主观的,因为我在谈论我的代码:))。

让我们试着弄清楚这个任务需要什么:

    首先,您需要将所有属性声明为可选。这是必需的,因为解码器可能必须处理部分响应。

    struct Character: Codable 
        let name:    String?
        let age:     Int?
        let hobbies: [String]?
    
    

    接下来,我们需要一种方法来确定如何将结构属性映射到部分 JSON 中的各个字段。幸运的是,Codable API 可以通过CodingKeys 枚举帮助我们:

    enum CodingKeys: String, CodingKey 
        case name
        case age
        case hobbies
    
    

    第一个棘手的部分是以某种方式将 CodingKeys 枚举转换为字符串数组,我们可以将其用于数组响应 - "value":["[Ljava.lang.Object;",["Bilbo",111]]。我们在这里很幸运,互联网上有各种资源和 SO 解决了获取枚举的所有案例的问题。我首选的解决方案是RawRepresentable 扩展,因为CodingKey 是原始可表示的,它的原始值是String

    // Adds support for retrieving all enum cases. Since we refer a protocol here,
    // theoretically this method can be called on other types than enum
    public extension RawRepresentable 
        static var enumCases: [Self] 
            var caseIndex: Int = 0
            return Array(AnyIterator 
                defer  caseIndex += 1 
                return withUnsafePointer(to: &caseIndex) 
                    $0.withMemoryRebound(to: Self.self, capacity: 1)  $0.pointee 
                
            )
        
    
    

    我们快到了,但我们还需要做一些工作才能解码。

    现在我们有一个Decodable 类型,一个要使用的编码键列表,我们需要一个使用这些的解码器。但在此之前,我们需要能够识别可以部分解码的类型。让我们添加一个新协议

    protocol PartiallyDecodable: Decodable 
        associatedtype PartialKeys: RawRepresentable
    
    

    并使Character符合它

    struct Character : Codable, PartiallyDecodable 
        typealias PartialKeys = CodingKeys
    

    最后的部分是解码部分。我们可以复用标准库自带的JSONDecoder

    // Tells the form of data the server sent and we want  to decode:
    enum PartialDecodingStrategy 
        case singleKey(String)
        case arrayOfValues
        case dictionary
    
    
    extension JSONDecoder 
    
        // Decodes an object by using a decoding strategy
        func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String 
    

将以上所有内容连接起来会产生以下基础架构:

// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable 
    static var enumCases: [Self] 
        var caseIndex: Int = 0
        return Array(AnyIterator 
            defer  caseIndex += 1 
            return withUnsafePointer(to: &caseIndex) 
                $0.withMemoryRebound(to: Self.self, capacity: 1)  $0.pointee 
            
        )
    


protocol PartiallyDecodable: Decodable 
    associatedtype PartialKeys: RawRepresentable


// Tells the form of data the server sent and we want  to decode:
enum PartialDecodingStrategy 
    case singleKey(String)
    case arrayOfValues
    case dictionary


extension JSONDecoder 

    // Decodes an object by using a decoding strategy
    func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String 
        guard let partialJSON = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [AnyHashable:Any] else 
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON"))
        
        guard let value = partialJSON["value"] else 
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Missing \"value\" key"))
        
        let processedJSON: [AnyHashable:Any]
        switch strategy 
        case let .singleKey(key):
            processedJSON = [key:value]
        case .arrayOfValues:
            guard let values = value as? [Any],
                values.count == 2,
                let properties = values[1] as? [Any] else 
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a 2 elements array for the \"value\" key"))
            

            processedJSON = zip(T.PartialKeys.enumCases, properties)
                .reduce(into: [:])  $0[$1.0.rawValue] = $1.1 
        case .dictionary:
            guard let dict = value as? [AnyHashable:Any] else 
                 throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a dictionary for the \"value\" key"))
            
            processedJSON = dict
        
        return try decode(type, from: JSONSerialization.data(withJSONObject: processedJSON, options: []))
    

我们希望能够部分解码Character,所以我们让它采用所有必需的协议:

struct Character: Codable, PartiallyDecodable 
    typealias PartialKeys = CodingKeys
    let name:    String?
    let age:     Int?
    let hobbies: [String]?

    enum CodingKeys: String, CodingKey 
        case name
        case age
        case hobbies
    

现在有趣的部分,让我们测试一下:

let decoder = JSONDecoder()

let jsonData1 = "\"value\":\"Bilbo\"".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
                                  withStrategy: .singleKey(Character.CodingKeys.name.rawValue),
                                  from: jsonData1)) as Any)

let jsonData2 = "\"value\":[\"[Ljava.lang.Object;\",[\"Bilbo\",111]]".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
                                  withStrategy: .arrayOfValues,
                                  from: jsonData2)) as Any)

let jsonData3 = "\"value\":\"name\":\"Bilbo\",\"age\":111,\"hobbies\":[\"rings\"]".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
                                  withStrategy: .dictionary,
                                  from: jsonData3)) as Any)

正如我们所料,输出如下:

Optional(MyApp.Character(name: Optional("Bilbo"), age: nil, hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: Optional(["rings"])))

正如我们所看到的,在布置了适当的基础架构后,要部分解码的类型的唯一要求是符合 PartiallyDecodable 并有一个枚举来说明要解码的密钥。这些要求很容易满足。

【讨论】:

以上是关于如何处理完全动态的 JSON 响应的主要内容,如果未能解决你的问题,请参考以下文章

对象。 C - 如何处理错误写入的 JSON

如何处理多个单独的 json 响应?

如何处理大 JSON 响应

如何处理具有完全不同构建系统的第三方库?

如何使用 jsonDecoder 处理来自 JSON 响应的动态键?

如何处理android中reddit api的深层嵌套json响应?