首页 文章

如何在Swift 4可解码协议中解码具有JSON字典类型的属性

提问于
浏览
59

假设我有 Customer 数据类型,其中包含 metadata 属性,该属性可以包含客户对象中的任何JSON字典

struct Customer {
  let id: String
  let email: String
  let metadata: [String: Any]
}

{  
  "object": "customer",
  "id": "4yq6txdpfadhbaqnwp3",
  "email": "john.doe@example.com",
  "metadata": {
    "link_id": "linked-id",
    "buy_count": 4
  }
}

The metadata property can be any arbitrary JSON map object.

在我从 NSJSONDeserialization 中使用反序列化的JSON转换属性之前,但是使用新的Swift 4 Decodable 协议,我仍然无法想到这样做的方法 .

有人知道如何使用可解码协议在Swift 4中实现这一目标吗?

10 回答

  • 0

    我从this gist获得了一些灵感,我为 UnkeyedDecodingContainerKeyedDecodingContainer 写了一些扩展 . 你可以找到我的要点here的链接 . 通过使用此代码,您现在可以使用熟悉的语法解码任何 Array<Any>Dictionary<String, Any>

    let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
    

    要么

    let array: [Any] = try container.decode([Any].self, forKey: key)
    

    Edit: 我发现有一个警告是解码字典数组 [[String: Any]] 所需的语法如下 . 您可能希望抛出错误而不是强制转换:

    let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]
    

    EDIT 2: 如果您只是想将整个文件转换为字典,最好坚持使用JSONSerialization中的api,因为我还没有找到一种方法来扩展JSONDecoder本身以直接解码字典 .

    guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
      // appropriate error handling
      return
    }
    

    扩展名

    // Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
    
    struct JSONCodingKeys: CodingKey {
        var stringValue: String
    
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
    
        var intValue: Int?
    
        init?(intValue: Int) {
            self.init(stringValue: "\(intValue)")
            self.intValue = intValue
        }
    }
    
    
    extension KeyedDecodingContainer {
    
        func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
            let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
            return try container.decode(type)
        }
    
        func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
            guard contains(key) else { 
                return nil
            }
            guard try decodeNil(forKey: key) == false else { 
                return nil 
            }
            return try decode(type, forKey: key)
        }
    
        func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
            var container = try self.nestedUnkeyedContainer(forKey: key)
            return try container.decode(type)
        }
    
        func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
            guard contains(key) else {
                return nil
            }
            guard try decodeNil(forKey: key) == false else { 
                return nil 
            }
            return try decode(type, forKey: key)
        }
    
        func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
            var dictionary = Dictionary<String, Any>()
    
            for key in allKeys {
                if let boolValue = try? decode(Bool.self, forKey: key) {
                    dictionary[key.stringValue] = boolValue
                } else if let stringValue = try? decode(String.self, forKey: key) {
                    dictionary[key.stringValue] = stringValue
                } else if let intValue = try? decode(Int.self, forKey: key) {
                    dictionary[key.stringValue] = intValue
                } else if let doubleValue = try? decode(Double.self, forKey: key) {
                    dictionary[key.stringValue] = doubleValue
                } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
                    dictionary[key.stringValue] = nestedDictionary
                } else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
                    dictionary[key.stringValue] = nestedArray
                }
            }
            return dictionary
        }
    }
    
    extension UnkeyedDecodingContainer {
    
        mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
            var array: [Any] = []
            while isAtEnd == false {
                // See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
                if try decodeNil() {
                    continue
                } else if let value = try? decode(Bool.self) {
                    array.append(value)
                } else if let value = try? decode(Double.self) {
                    array.append(value)
                } else if let value = try? decode(String.self) {
                    array.append(value)
                } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
                    array.append(nestedDictionary)
                } else if let nestedArray = try? decode(Array<Any>.self) {
                    array.append(nestedArray)
                }
            }
            return array
        }
    
        mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
    
            let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
            return try nestedContainer.decode(type)
        }
    }
    
  • 4

    我也玩过这个问题,最后写了一个simple library for working with “generic JSON” types . (其中“generic”表示“没有事先知道结构” . )要点是用具体类型表示通用JSON:

    public enum JSON {
        case string(String)
        case number(Float)
        case object([String:JSON])
        case array([JSON])
        case bool(Bool)
        case null
    }
    

    然后,此类型可以实现 CodableEquatable .

  • 10

    当我找到旧的答案时,我只测试了一个简单的JSON对象案例,但不是一个空案例,这将导致运行时异常,如@slurmomatic和@zoul找到 . 对不起,这个问题 .

    所以我通过一个简单的JSONValue协议尝试另一种方式,实现 AnyJSONValue 类型的擦除结构并使用该类型而不是 Any . 这是一个实现 .

    public protocol JSONType: Decodable {
        var jsonValue: Any { get }
    }
    
    extension Int: JSONType {
        public var jsonValue: Any { return self }
    }
    extension String: JSONType {
        public var jsonValue: Any { return self }
    }
    extension Double: JSONType {
        public var jsonValue: Any { return self }
    }
    extension Bool: JSONType {
        public var jsonValue: Any { return self }
    }
    
    public struct AnyJSONType: JSONType {
        public let jsonValue: Any
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
    
            if let intValue = try? container.decode(Int.self) {
                jsonValue = intValue
            } else if let stringValue = try? container.decode(String.self) {
                jsonValue = stringValue
            } else if let boolValue = try? container.decode(Bool.self) {
                jsonValue = boolValue
            } else if let doubleValue = try? container.decode(Double.self) {
                jsonValue = doubleValue
            } else if let doubleValue = try? container.decode(Array<AnyJSONType>.self) {
                jsonValue = doubleValue
            } else if let doubleValue = try? container.decode(Dictionary<String, AnyJSONType>.self) {
                jsonValue = doubleValue
            } else {
                throw DecodingError.typeMismatch(JSONType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported JSON tyep"))
            }
        }
    }
    

    以下是解码时如何使用它

    metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)
    

    这个问题的问题是我们必须调用 value.jsonValue as? Int . 我们需要等到Swift的土地,这将解决这个问题,或者至少帮助它变得更好 .


    [旧答案]

    我在Apple Developer论坛上发布了这个问题,事实证明这很容易 .

    我可以

    metadata = try container.decode ([String: Any].self, forKey: .metadata)
    

    在初始化程序中 .

    首先想念那个是我的坏事 .

  • 5

    我的解决方案略有不同 .

    让我们假设我们有一些不仅仅是简单的 [String: Any] 来解析Any可能是一个数组或一个嵌套字典或一个数组字典 .

    像这样的东西:

    var json = """
    {
      "id": 12345,
      "name": "Giuseppe",
      "last_name": "Lanza",
      "age": 31,
      "happy": true,
      "rate": 1.5,
      "classes": ["maths", "phisics"],
      "dogs": [
        {
          "name": "Gala",
          "age": 1
        }, {
          "name": "Aria",
          "age": 3
        }
      ]
    }
    """
    

    嗯,这是我的解决方案:

    public struct AnyDecodable: Decodable {
      public var value: Any
    
      private struct CodingKeys: CodingKey {
        var stringValue: String
        var intValue: Int?
        init?(intValue: Int) {
          self.stringValue = "\(intValue)"
          self.intValue = intValue
        }
        init?(stringValue: String) { self.stringValue = stringValue }
      }
    
      public 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(AnyDecodable.self, forKey: key).value
          }
          value = result
        } else if var container = try? decoder.unkeyedContainer() {
          var result = [Any]()
          while !container.isAtEnd {
            result.append(try container.decode(AnyDecodable.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"))
        }
      }
    }
    

    尝试使用它

    let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
    print(stud)
    
  • -1

    您可以创建确认 Codable 协议的元数据结构,并使用 Decodable 类创建如下所示的对象

    let json: [String: Any] = [
        "object": "customer",
        "id": "4yq6txdpfadhbaqnwp3",
        "email": "john.doe@example.com",
        "metadata": [
            "link_id": "linked-id",
            "buy_count": 4
        ]
    ]
    
    struct Customer: Codable {
        let object: String
        let id: String
        let email: String
        let metadata: Metadata
    }
    
    struct Metadata: Codable {
        let link_id: String
        let buy_count: Int
    }
    
    let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
    
    let decoder = JSONDecoder()
    do {
        let customer = try decoder.decode(Customer.self, from: data)
        print(customer)
    } catch {
        print(error.localizedDescription)
    }
    
  • 0

    你可以看看BeyovaJSON

    import BeyovaJSON
    
    struct Customer: Codable {
      let id: String
      let email: String
      let metadata: JToken
    }
    
    //create a customer instance
    
    customer.metadata = ["link_id": "linked-id","buy_count": 4]
    
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted 
    print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)
    
  • 1

    最简单和建议的方法是 create separate model for each dictionary or model that is in JSON .

    这就是我的工作

    //Model for dictionary **Metadata**
    
    struct Metadata: Codable {
        var link_id: String?
        var buy_count: Int?
    }  
    
    //Model for dictionary **Customer**
    
    struct Customer: Codable {
       var object: String?
       var id: String?
       var email: String?
       var metadata: Metadata?
    }
    
    //Here is our decodable parser that decodes JSON into expected model
    
    struct CustomerParser {
        var customer: Customer?
    }
    
    extension CustomerParser: Decodable {
    
    //keys that matches exactly with JSON
    enum CustomerKeys: String, CodingKey {
        case object = "object"
        case id = "id"
        case email = "email"
        case metadata = "metadata"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CustomerKeys.self) // defining our (keyed) container
    
        let object: String = try container.decode(String.self, forKey: .object) // extracting the data
        let id: String = try container.decode(String.self, forKey: .id) // extracting the data
        let email: String = try container.decode(String.self, forKey: .email) // extracting the data
    
       //Here I have used metadata model instead of dictionary [String: Any]
        let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // extracting the data
    
        self.init(customer: Customer(object: object, id: id, email: email, metadata: metadata))
    
        }
    }
    

    Usage:

    if let url = Bundle.main.url(forResource: "customer-json-file", withExtension: "json") {
            do {
                let jsonData: Data =  try Data(contentsOf: url)
                let parser: CustomerParser = try JSONDecoder().decode(CustomerParser.self, from: jsonData)
                print(parser.customer ?? "null")
    
            } catch {
    
            }
        }
    

    **我在解析时使用了可选的安全端,可以根据需要进行更改 .

    Read more on this topic

  • 40

    这是更通用的(不仅 [String: Any] ,但 [Any] 可以解码)和封装的方法(单独的实体用于此)受到@loudmouth答案的启发 .

    使用它看起来像:

    extension Customer: Decodable {
      public init(from decoder: Decoder) throws {
        let selfContainer = try decoder.container(keyedBy: CodingKeys.self)
        id = try selfContainer.decode(.id)
        email = try selfContainer.decode(.email)
        let metadataContainer: JsonContainer = try selfContainer.decode(.metadata)
        guard let metadata = metadataContainer.value as? [String: Any] else {
          let context = DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Expected '[String: Any]' for 'metadata' key")
          throw DecodingError.typeMismatch([String: Any].self, context)
        }
        self.metadata = metadata
      }
    
      private enum CodingKeys: String, CodingKey {
        case id, email, metadata
      }
    }
    

    JsonContainer 是一个帮助器实体,我们用它来解码JSON数据到JSON对象(数组或字典)而不扩展 *DecodingContainer (所以它不会干扰J11对象不是 [String: Any] 所指的罕见情况) .

    struct JsonContainer {
    
      let value: Any
    }
    
    extension JsonContainer: Decodable {
    
      public init(from decoder: Decoder) throws {
        if let keyedContainer = try? decoder.container(keyedBy: Key.self) {
          var dictionary = [String: Any]()
          for key in keyedContainer.allKeys {
            if let value = try? keyedContainer.decode(Bool.self, forKey: key) {
              // Wrapping numeric and boolean types in `NSNumber` is important, so `as? Int64` or `as? Float` casts will work
              dictionary[key.stringValue] = NSNumber(value: value)
            } else if let value = try? keyedContainer.decode(Int64.self, forKey: key) {
              dictionary[key.stringValue] = NSNumber(value: value)
            } else if let value = try? keyedContainer.decode(Double.self, forKey: key) {
              dictionary[key.stringValue] = NSNumber(value: value)
            } else if let value = try? keyedContainer.decode(String.self, forKey: key) {
              dictionary[key.stringValue] = value
            } else if (try? keyedContainer.decodeNil(forKey: key)) ?? false {
              // NOP
            } else if let value = try? keyedContainer.decode(JsonContainer.self, forKey: key) {
              dictionary[key.stringValue] = value.value
            } else {
              throw DecodingError.dataCorruptedError(forKey: key, in: keyedContainer, debugDescription: "Unexpected value for \(key.stringValue) key")
            }
          }
          value = dictionary
        } else if var unkeyedContainer = try? decoder.unkeyedContainer() {
          var array = [Any]()
          while !unkeyedContainer.isAtEnd {
            let container = try unkeyedContainer.decode(JsonContainer.self)
            array.append(container.value)
          }
          value = array
        } else if let singleValueContainer = try? decoder.singleValueContainer() {
          if let value = try? singleValueContainer.decode(Bool.self) {
            self.value = NSNumber(value: value)
          } else if let value = try? singleValueContainer.decode(Int64.self) {
            self.value = NSNumber(value: value)
          } else if let value = try? singleValueContainer.decode(Double.self) {
            self.value = NSNumber(value: value)
          } else if let value = try? singleValueContainer.decode(String.self) {
            self.value = value
          } else if singleValueContainer.decodeNil() {
            value = NSNull()
          } else {
            throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Unexpected value")
          }
        } else {
          let context = DecodingError.Context(codingPath: [], debugDescription: "Invalid data format for JSON")
          throw DecodingError.dataCorrupted(context)
        }
      }
    
      private struct Key: CodingKey {
        var stringValue: String
    
        init?(stringValue: String) {
          self.stringValue = stringValue
        }
    
        var intValue: Int?
    
        init?(intValue: Int) {
          self.init(stringValue: "\(intValue)")
          self.intValue = intValue
        }
      }
    }
    

    请注意,数字和布尔类型由 NSNumber 支持,否则这样的东西将不起作用:

    if customer.metadata["keyForInt"] as? Int64 { // as it always will be nil
    
  • -1

    你想要什么违背 Codable 的设计 . Codable 背后的想法是提供一种以类型安全的方式存档和取消归档数据的机制 . 这意味着您必须事先定义属性及其数据类型 . 我可以想到你的问题的两个解决方案:

    1.列出所有潜在的元数据键

    通常,如果您深入到API的文档中,您将找到所有潜在元数据键的完整列表 . 定义 Metadata 结构,将这些键作为可选属性:

    struct Customer: Decodable {
        struct Metadata: Decodable {
            var linkId: String?
            var buyCount: Int?
            var somethingElse: Int?
    
            private enum CodingKeys: String, CodingKey {
                case linkId = "link_id"
                case buyCount = "buy_count"
                case somethingElse = "something_else"
            }
        }
    
        var object: String
        var id: String
        var email: String
        var metadata: Metadata
    }
    
    let customer = try! JSONDecoder().decode(Customer.self, from: jsonData)
    print(customer.metadata)
    

    我可以看到Swift设计师更喜欢这种方法 .


    2.结合可解码和JSONSerialization

    JSONSerialization 在类型安全的权衡中提供了巨大的动力 . 你绝对可以将它与 Decodable 混淆,后者的设计理念就是相反:

    struct Customer {
        private struct RawCustomer: Decodable {
            var object: String
            var id: String
            var email: String
        }
    
        var object: String
        var id: String
        var email: String
        var metadata: [String: AnyObject]
    
        init(jsonData: Data) throws {
            let rawCustomer = try JSONDecoder().decode(RawCustomer.self, from: jsonData)
            object = rawCustomer.object
            id     = rawCustomer.id
            email  = rawCustomer.email
    
            let jsonObject = try JSONSerialization.jsonObject(with: jsonData)
            if let dict = jsonObject as? [String: AnyObject],
                let metadata = dict["metadata"] as? [String: AnyObject]
            {
                self.metadata = metadata
            } else {
                self.metadata = [String: AnyObject]()
            }
        }
    }
    
    let customer = try! Customer(jsonData: jsonData)
    print(customer.metadata)
    
  • 6

    如果使用SwiftyJSON来解析JSON,则可以更新为4.1.0,它具有 Codable 协议支持 . 只要声明 metadata: JSON 就可以了 .

    import SwiftyJSON
    
    struct Customer {
      let id: String
      let email: String
      let metadata: JSON
    }
    

相关问题