Swift 4可解码协议中如何使用JSON字典类型解码属性


103

假设我的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
  }
}

metadata属性可以是任何任意的JSON映射对象。

在我可以从反序列化的JSON强制转换属性(NSJSONDeserialization但使用新的Swift 4 Decodable协议)之前,我仍然想不出一种方法。

有谁知道如何在Swift 4中使用Decodable协议实现这一目标?

Answers:


89

从我发现的要点中得到一些启发,我为UnkeyedDecodingContainer和编写了一些扩展名KeyedDecodingContainer。您可以在这里找到我的要旨的链接。通过使用以下代码,您现在可以解码任何一种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)

编辑:有一个警告,我发现它正在解码字典数组。[[String: Any]]所需的语法如下。您可能想抛出一个错误而不是强制转换:

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

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

有趣的是,我将尝试这个要点,并将结果更新给您@loudmouth
Pitiphong Phongpattranont

@PitiphongPhongpattranont这段代码对您有用吗?
Loudmouth

1
@JonBrooks在过去状态UnkeyedDecodingContainerdecode(_ type: Array<Any>.Type) throws -> Array<Any>被检查一个嵌套阵列。因此,如果您的数据结构如下所示:[true, 452.0, ["a", "b", "c"] ] 它将拉出嵌套["a", "b", "c"]数组。的decode一个的方法UnkeyedDecodingContainer“啪啪”关从容器的元素。它不应该引起无限递归。
嘴巴

1
@loudmouth可以在json:中为键设置nil值{"array": null}。因此,您guard contains(key)会通过,但是稍后尝试解码键“数组”的空值时,它将崩溃几行。因此,最好在调用之前再添加一个条件,以检查该值是否实际上不是null decode
chebur

2
我找到了解决方法:而不是} else if let nestedArray = try? decode(Array<Any>.self, forKey: key)尝试:} else if var nestedContainer = try? nestedUnkeyedContainer(), let nestedArray = try? nestedContainer.decode(Array<Any>.self) {
乔恩·布鲁克斯

23

我也解决了这个问题,最后编写了一个简单的库来处理“通用JSON”类型。(其中“通用”的意思是“没有预先知道的结构”。)要点是使用具体类型表示通用JSON:

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

然后,此类型可以实现CodableEquatable


13

您可以创建元数据结构,以确认Decodable协议并使用JSONDecoder类通过以下类似的解码方法从数据创建对象

let json: [String: Any] = [
    "object": "customer",
    "id": "4yq6txdpfadhbaqnwp3",
    "email": "john.doe@example.com",
    "metadata": [
        "link_id": "linked-id",
        "buy_count": 4
    ]
]

struct Customer: Decodable {
    let object: String
    let id: String
    let email: String
    let metadata: Metadata
}

struct Metadata: Decodable {
    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)
}

10
不,我不能,因为我不知道metadata值的结构。它可以是任意对象。
Pitiphong Phongpattranont

您是说它可以是数组类型还是字典类型?
Suhit Patil

您能否举个例子或添加有关元数据结构的更多解释
Suhit Patil

2
的值metadata可以是任何JSON对象。因此它可以是空字典或任何字典。“元数据”:{}“元数据”:{user_id:“ id”}“元数据”:{首选项:{shows_value:true,语言:“ en”}}等等
Pitiphong Phongpattranont

一种可能的选择是使用元数据结构中的所有参数作为可选参数,并列出元数据结构中的所有可能值,例如结构元数据{var user_id:String?var偏好设定:字串?}
Suhit Patil

8

我提出了一个略有不同的解决方案。

假设我们不仅仅拥有简单的东西 [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)

6

当我找到旧答案时,我仅测试了一个简单的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。我们需要等待,直到Conditional Conformance登陆Swift,这将解决这个问题,或者至少帮助它变得更好。


[旧答案]

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

我可以

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

在初始化程序中。

首先错过那是我的遗憾。


4
可以在Apple Developer上发布问题链接。Any不符合,Decodable所以我不确定这是正确的答案。
Reza Shirazian

@RezaShirazian这就是我最初的想法。但是事实证明,当Dictionary的键符合Hashable且不依赖于其值时,Dictionary符合Encodable。您可以打开Dictionary标头并自己查看。扩展字典:Encodable,其中密钥:可散列扩展词典:可解码,其中密钥:Hashable forums.developer.apple.com/thread/80288#237680
Pitiphong Phongpattranont

6
目前,这不起作用。“ Dictionary <String,Any>不符合Decodable,因为Any不符合Decodable”
mbuchetics

事实证明它可行。我在代码中使用它。您需要了解,现在没有办法表达“字典的值必须符合Decodable协议才能使Dictionary符合Decodable协议”的要求。这就是Swift 4中尚未实现的“条件一致性”,我认为现在还可以,因为Swift类型系统(和泛型)存在很多限制。因此,这现在就可以使用,但是当Swift类型系统在将来进行改进时(尤其是在实现条件一致性时),这将不起作用。
Pitiphong Phongpattranont

3
从Xcode 9 beta 5开始,它不适用于我。编译,但在运行时爆炸:Dictionary <String,Any>不符合Decodable,因为Any不符合Decodable。
zoul

6

如果您使用SwiftyJSON解析JSON,则可以更新到4.1.0,该Codable协议支持协议。只需声明metadata: JSON就可以了。

import SwiftyJSON

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

我不知道为什么这个答案被否决了。这完全有效,可以解决问题。
Leonid Usov '18

这似乎是从SwiftyJSON到可解迁移好
米哈尔Ziobro

这并不能解决如何解析元数据json的原始问题。
llamacorn

1

您可能会看看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)!)

哦,真的很好。使用它来接收作为JToken的通用JSON,附加一些值并返回到服务器。确实很不错。那是您完成的
出色

1

最简单和建议的方法是为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))

    }
}

用法:

  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 {

        }
    }

**我在解析时出于安全考虑使用了Optional,可以根据需要进行更改。

阅读有关此主题的更多信息


1
您的答案肯定是适用于Swift 4.1的答案,并且您的帖子的第一行已经死了!假设数据来自Web服务。您可以为简单的嵌套对象建模,然后使用点语法抓取每个对象。请参阅下面的suhit答案。
David H

1

我做了一个吊舱,以方便解码+编码方式[String: Any][Any]。这提供了对可选属性的编码或解码,请参见此处https://github.com/levantAJ/AnyCodable

pod 'DynamicCodable', '1.0'

如何使用它:

import DynamicCodable

struct YourObject: Codable {
    var dict: [String: Any]
    var array: [Any]
    var optionalDict: [String: Any]?
    var optionalArray: [Any]?

    enum CodingKeys: String, CodingKey {
        case dict
        case array
        case optionalDict
        case optionalArray
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        dict = try values.decode([String: Any].self, forKey: .dict)
        array = try values.decode([Any].self, forKey: .array)
        optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
        optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dict, forKey: .dict)
        try container.encode(array, forKey: .array)
        try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
        try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
    }
}

0

这是更通用的(不仅是[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(因此,在JSON对象不是的情况下,它不会干扰极少数情况[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

我可以仅解码选定的属性,而自动保留其他解码属性,因为我有15个满足autoDecoding的属性,也许还有3个需要一些自定义解码处理的属性?
米乔·齐布罗(MichałZiobro)'18年

@MichałZiobro是否要将部分数据解码为JSON对象,并将部分数据解码为单独的实例变量?或者您是在问只为对象的一部分编写部分解码初始化程序(它与JSON之类的结构没有什么共同点)?据我所知,第一个问题的答案是肯定的,而第二个问题的答案是肯定的。
阿列克谢·科泽夫尼科夫

我想有只与定制解码的一些性质,并与标准的默认解码休息
米哈尔Ziobro

@MichałZiobro如果我正确地理解你的话,那是不可能的。无论如何,您的问题与当前的SO问题无关,值得单独提出。
阿列克谢·科泽夫尼科夫

0

使用解码器和编码密钥进行解码

public let dataToDecode: [String: AnyDecodable]

enum CodingKeys: CodingKey {
    case dataToDecode
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.dataToDecode = try container.decode(Dictionary<String, AnyDecodable>.self, forKey: .dataToDecode) 
}    

-1
extension ViewController {

    func swiftyJson(){
        let url = URL(string: "https://itunes.apple.com/search?term=jack+johnson")
        //let url = URL(string: "http://makani.bitstaging.in/api/business/businesses_list")

        Alamofire.request(url!, method: .get, parameters: nil).responseJSON { response in
            var arrayIndexes = [IndexPath]()
            switch(response.result) {
            case .success(_):

                let data = response.result.value as! [String : Any]

                if let responseData =  Mapper<DataModel>().map(JSON: data) {
                    if responseData.results!.count > 0{
                        self.arrayExploreStylistList = []
                    }
                    for i in 0..<responseData.results!.count{
                        arrayIndexes.append(IndexPath(row: self.arrayExploreStylistList.count + i, section: 0))
                    }
                    self.arrayExploreStylistList.append(contentsOf: responseData.results!)

                    print(arrayIndexes.count)

                }

                //                    if let arrNew = data["results"] as? [[String : Any]]{
                //                        let jobData = Mapper<DataModel>().mapArray(JSONArray: arrNew)
                //                        print(jobData)
                //                        self.datamodel = jobData
                //                    }
                self.tblView.reloadData()
                break

            case .failure(_):
                print(response.result.error as Any)
                break

            }
        }

    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.