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

Pit*_*ont 84 json swift swift4 codable

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

struct Customer {
  let id: String
  let email: String
  let metadata: [String: Any]
}
Run Code Online (Sandbox Code Playgroud)
{  
  "object": "customer",
  "id": "4yq6txdpfadhbaqnwp3",
  "email": "john.doe@example.com",
  "metadata": {
    "link_id": "linked-id",
    "buy_count": 4
  }
}
Run Code Online (Sandbox Code Playgroud)

metadata属性可以是任意JSON映射对象.

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

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

lou*_*uth 66

从我发现的这个要点中获得了一些灵感,我为UnkeyedDecodingContainer和写了一些扩展KeyedDecodingContainer.你可以在这里找到我的要点的链接.通过使用此代码,您现在可以解码任何Array<Any>Dictionary<String, Any>使用熟悉的语法:

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
Run Code Online (Sandbox Code Playgroud)

要么

let array: [Any] = try container.decode([Any].self, forKey: key)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

扩展

// 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我找到了一个解决方法:如果让nestedArray = try,而不是`}?encode(Array &lt;Any&gt; .self,forKey:key)`try:`}如果var nestedContainer = try?nestedUnkeyedContainer(),让nestedArray =试试吗?nestedContainer.decode(Array &lt;Any&gt; .self){` (2认同)

zou*_*oul 17

我也玩过这个问题,最后写了一个简单的库来处理"泛型JSON"类型.(其中"generic"表示"没有事先知道结构".)要点是用具体类型表示通用JSON:

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

然后这种类型可以实现CodableEquatable.


all*_*ang 8

如果您使用SwiftyJSON来解析 JSON,则可以更新到具有协议支持的4.1.0Codable。只需声明即可metadata: JSON

import SwiftyJSON

struct Customer {
  let id: String
  let email: String
  let metadata: JSON
}
Run Code Online (Sandbox Code Playgroud)


Suh*_*til 6

您可以创建确认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)
}
Run Code Online (Sandbox Code Playgroud)

  • 不,我不能,因为我不知道“元数据”值的结构。它可以是任意对象。 (5认同)
  • `metadata` 的值可以是任何 JSON 对象。所以它可以是空字典或任何字典。"metadata": {} "metadata": { user_id: "id" } "metadata": {偏好: { shows_value: true, language: "en" } } 等等。 (3认同)

Giu*_*nza 6

我的解决方案略有不同.

让我们假设我们有一些不仅仅是一个简单[String: 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
    }
  ]
}
"""
Run Code Online (Sandbox Code Playgroud)

嗯,这是我的解决方案:

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"))
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

尝试使用它

let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)
Run Code Online (Sandbox Code Playgroud)


Pit*_*ont 5

当我找到旧的答案时,我只测试了一个简单的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"))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是解码时如何使用它

metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)
Run Code Online (Sandbox Code Playgroud)

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


[旧答案]

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

我可以

metadata = try container.decode ([String: Any].self, forKey: .metadata)
Run Code Online (Sandbox Code Playgroud)

在初始化程序中.

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

  • 目前这不起作用."Dictionary <String,Any>不符合Decodable,因为Any不符合Decodable" (6认同)
  • 可以在Apple Developer上发布问题链接."任何"不符合"可解码",所以我不确定这是正确的答案. (4认同)
  • 在Xcode 9 beta 5中对我不起作用.编译,但在运行时爆炸:_Dictionary <String,Any>不符合Decodable,因为Any不符合Decodable._ (3认同)