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

时间:2018-01-30 20:04:55

标签: json swift swift4

也许社区中的某些人有类似的挣扎,并提出了一个可行的解决方案。

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

考虑以下结构

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'的值。财产,并返回。

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

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

{"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是否可以使用。

思想?

3 个答案:

答案 0 :(得分:6)

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并没有真正使界面更好(特别是因为下标无法抛出)。根据您的特定用例,可能需要进行一些调整。

答案 1 :(得分:1)

我为此目的自己写了一个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)

答案 2 :(得分:1)

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

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

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

    struct Character: Codable {
        let name:    String?
        let age:     Int?
        let hobbies: [String]?
    }
    
  2. 接下来,我们需要一种方法来弄清楚如何将struct属性映射到部分JSON中的各个字段。幸运的是,Codable API可以通过CodingKeys枚举来帮助我们:

    enum CodingKeys: String, CodingKey {
        case name
        case age
        case hobbies
    }
    
  3. 第一个棘手的部分是以某种方式将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 }
                }
            })
        }
    }
    

    我们几乎就在那里,但在解码之前我们需要做更多的工作。

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

    protocol PartiallyDecodable: Decodable {
        associatedtype PartialKeys: RawRepresentable
    }
    

    并使Character符合

    struct Character : Codable, PartiallyDecodable {
        typealias PartialKeys = CodingKeys
    
  5. 整理片是解码部分。我们可以重用标准库附带的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 {
    
  6. 将以上所有结果连接到以下基础架构中:

    // 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并具有一个枚举,说明要解码的键。这些要求很容易遵循。