在 Swift [45]可解码协议中,如何解码 JSON 字典类型的属性

假设我有一个 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 映射对象。

在从 NSJSONDeserialization反序列化的 JSON 中强制转换属性之前,使用新的 Swift 4 Decodable协议,我仍然想不出一种方法来实现这一点。

有人知道如何实现这与解码协议的 Swift 4?

136216 次浏览

您可以创建符合 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)
}

当我找到旧的答案时,我只测试了一个简单的 JSON 对象用例,而没有测试一个空的用例,因为它会导致像@sluromatic 和@zoul found 这样的运行时异常。对不起,这个问题。

因此,我尝试了另一种方法,使用一个简单的 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土地在斯威夫特,这将解决这个问题,或至少有助于它更好。


[老答案]

我在苹果开发者论坛上发布了这个问题,结果发现它非常简单。

我能做到

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

在初始化程序中。

一开始错过这个是我的错。

这个要点中得到一些灵感,我为 UnkeyedDecodingContainerKeyedDecodingContainer写了一些扩展。你可以找到一个链接到我的要点 给你。通过使用这段代码,您现在可以使用熟悉的语法对任何 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,因为我还没有找到一种方法来扩展 JSONDecder 本身来直接解码一个字典。

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

我也玩过这个问题,最后写了一个 用于处理“通用 JSON”类型的简单库。(“通用”的意思是“事先不知道结构”。)要点是用一个具体的类型表示通用 JSON:

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

然后这种类型可以实现 CodableEquatable

你可以看看 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)!)

这里是更通用的(不仅 [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

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


}
}

* * 我已经使用了可选的,以便在解析时处于安全状态,可以根据需要进行更改。

阅读更多关于这个主题的内容

我带来了一个稍微不同的解决方案。

让我们假设我们有一些比简单的 [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
}
]
}
"""

这就是我的解决办法:

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)

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

import SwiftyJSON


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

我做了一个豆荚,方便的方式解码 + 编码 [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)
}
}
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


}
}


}
}

使用解码器和编码键解码

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

细节

  • Xcode 12.0.1(12A7300)
  • 斯威夫特5.3

基于 大乐

// code from: https://github.com/levantAJ/AnyCodable/blob/master/AnyCodable/DecodingContainer%2BAnyCollection.swift


private
struct AnyCodingKey: CodingKey {
let stringValue: String
private (set) var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.intValue = intValue
stringValue = String(intValue)
}
}


extension KeyedDecodingContainer {


private
func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [Any] {
var values = try nestedUnkeyedContainer(forKey: key)
return try values.decode(type)
}


private
func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> [String: Any] {
try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key).decode(type)
}


func decode(_ type: [String: Any].Type) throws -> [String: Any] {
var dictionary: [String: Any] = [:]
for key in allKeys {
if try decodeNil(forKey: key) {
dictionary[key.stringValue] = NSNull()
} else if let bool = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = bool
} else if let string = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = string
} else if let int = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = int
} else if let double = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = double
} else if let dict = try? decode([String: Any].self, forKey: key) {
dictionary[key.stringValue] = dict
} else if let array = try? decode([Any].self, forKey: key) {
dictionary[key.stringValue] = array
}
}
return dictionary
}
}


extension UnkeyedDecodingContainer {
mutating func decode(_ type: [Any].Type) throws -> [Any] {
var elements: [Any] = []
while !isAtEnd {
if try decodeNil() {
elements.append(NSNull())
} else if let int = try? decode(Int.self) {
elements.append(int)
} else if let bool = try? decode(Bool.self) {
elements.append(bool)
} else if let double = try? decode(Double.self) {
elements.append(double)
} else if let string = try? decode(String.self) {
elements.append(string)
} else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self),
let element = try? values.decode([String: Any].self) {
elements.append(element)
} else if var values = try? nestedUnkeyedContainer(),
let element = try? values.decode([Any].self) {
elements.append(element)
}
}
return elements
}
}

解决方案

struct DecodableDictionary: Decodable {
typealias Value = [String: Any]
let dictionary: Value?
init(from decoder: Decoder) throws {
dictionary = try? decoder.container(keyedBy: AnyCodingKey.self).decode(Value.self)
}
}

用法

struct Model: Decodable {
let num: Double?
let flag: Bool?
let dict: DecodableDictionary?
let dict2: DecodableDictionary?
let dict3: DecodableDictionary?
}


let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print(object.dict?.dictionary)      // prints [String: Any]
print(object.dict2?.dictionary)     // prints nil
print(object.dict3?.dictionary)     // prints nil

我已经写了一篇文章和 回购,它有助于添加[ String: Any ]对 Codable 的解码和编码支持。

Https://medium.com/nerd-for-tech/string-any-support-for-codable-4ba062ce62f2

这在可解码方面有所改进,并且作为 https://stackoverflow.com/a/46049763/9160905提供的解决方案增加了可编码支持

你所能达到的目标:

杰森:

enter image description here

示例代码:

enter image description here

会成功的

public struct AnyDecodable: Decodable {
public let value: Any


public init<T>(_ value: T?) {
self.value = value ?? ()
}
}


let contentDecodable = try values.decodeIfPresent(AnyDecodable.self, forKey: .content)

我使用了一些关于这个主题的答案来得到最简单的解决方案。我的问题是我接收到的是 [String: Any]类型字典,但是我可以很好地使用 [String: String]转换 String 中的每个其他 Any值。所以这就是我的解决方案:

struct MetadataType: Codable {
let value: String?


private init(_ value: String?) {
self.value = value
}


init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()


if let decodedValue = try? container.decode(Int.self) {
self.init(String(decodedValue))
} else if let decodedValue = try? container.decode(Double.self) {
self.init(String(decodedValue))
} else if let decodedValue = try? container.decode(Bool.self) {
self.init(String(decodedValue))
} else if let decodedValue = try? container.decode(String.self) {
self.init(decodedValue)
} else {
self.init(nil)
}
}
}

当我查字典的时候,我用

let userInfo: [String: MetadataType]