如何在Swift中使用可解码协议(decodable protocol)解码一个类型为JSON字典的属性?[45]

173

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

在使用新的Swift 4 Decodable协议之前,我可以从反序列化的JSON中投射该属性,但我仍然想不出如何做到这一点。

有人知道如何在Swift 4中使用Decodable协议实现吗?

16个回答

151

受到我在这个代码片段的启发,我为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,因为我尚未找到一种扩展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)
    }
}

@PitiphongPhongpattranont,这段代码对你有用吗? - loudmouth
我会说是的。我稍微重构了一下这段代码,但你的主要想法确实非常好。谢谢。 - Pitiphong Phongpattranont
1
@JonBrooks 在 UnkeyedDecodingContainerdecode(_ type: Array<Any>.Type) throws -> Array<Any> 中的最后一个条件是检查 嵌套 数组。因此,如果您有一个类似以下结构的数据: [true, 452.0, ["a", "b", "c"]]它将提取嵌套的 ["a", "b", "c"] 数组。UnkeyedDecodingContainerdecode 方法会从容器中“弹出”元素。这不应该导致无限递归。 - loudmouth
1
@loudmouth,在 JSON 中,键的值可以为 nil:{"array": null}。因此,你的 guard contains(key) 可能会通过,但是在几行后,当尝试解码 "array" 键的 null 值时,它将崩溃。因此,在调用 decode 之前最好添加一个条件来检查该值是否实际上不是 null。 - 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) - Jon Brooks
显示剩余10条评论

42

我也曾经研究过这个问题,最终编写了一个用于处理“通用JSON”类型的简单库。(其中,“通用”意味着“没有事先已知的结构”)。主要的思路是使用具体类型来表示通用JSON:

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

接下来,这种类型可以实现 CodableEquatable


1
这是一个非常优雅的解决方案。它非常简洁,运行良好,并且不像其他答案一样具有hacky的特点。我唯一的建议是将数字替换为单独的整数和浮点类型。在技术上,JS中的所有数字都是浮点数,但在Swift中将整数解码为整数更有效和更清晰。 - user3236716
你能展示一下如何将一个字符串解码成你的结构体吗? - user19473296

17

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

41
不行,因为我不了解“metadata”值的结构,它可以是任意对象。 - Pitiphong Phongpattranont
你的意思是它可以是数组或字典类型吗? - Suhit Patil
你能否举个例子或者更详细地解释一下元数据结构? - Suhit Patil
4
metadata的值可以是任何JSON对象。因此,它可以是空字典或任何字典。"metadata": {} "metadata": { user_id: "id" } "metadata": { preference: { shows_value: true, language: "en" } }等等。 - Pitiphong Phongpattranont
一个可能的选项是将元数据结构中的所有参数都作为可选项使用,并在元数据结构中列出所有可能的值,例如:struct metadata { var user_id: String? var preference: String? } - Suhit Patil
如果您知道所得数据的结构,那么是的,这是适当的解决方案!非常好!如果它可能会变化,那么您可以尝试几次解码来找到希望起作用的解码方式。 - David H

14

我提出了一个稍微不同的解决方案。

假设我们要解析的不仅仅是一个简单的 [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)

这个如何解码一个数组? - Mary Doe

10
如果您使用 SwiftyJSON 来解析 JSON,您可以升级到支持 Codable 协议的版本 4.1.0。只需声明 metadata: JSON 即可完成设置。
import SwiftyJSON

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

我不知道为什么这个答案被踩了。它完全有效并解决了问题。 - Leonid Usov
似乎将SwiftyJSON迁移到Decodable上是个不错的选择。 - Michał Ziobro
1
这并没有解决如何解析元数据 JSON 的问题,这也是最初的问题。 - llamacorn

9

我发现旧的答案只测试了一个简单的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 中的 Conditional Conformance 到来,这将解决此问题或至少有所帮助。

[旧答案]

我在Apple开发者论坛上发布了这个问题,结果很容易解决。

我可以这样做

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

在初始化程序中。

我一开始错过了这个问题,是我的失误。


4
可以在Apple Developer上发布问题的链接吗?“Any”不符合“Decodable”的要求,所以我不确定这是否是正确的答案。 - Reza Shirazian
@RezaShirazian 我一开始也是这么想的。但事实证明,当字典的键符合可哈希协议时,它符合可编码协议,而不依赖于其值。您可以打开字典头文件自行查看。扩展字典:其中键:可哈希符合可编码协议,其中键:可哈希符合可解码协议。forums.developer.apple.com/thread/80288#237680 - Pitiphong Phongpattranont
8
目前这个无法运行。 "Dictionary<String, Any>不符合可解码协议,因为Any类型不符合可解码协议"。 - mbuchetics
4
在Xcode 9 beta 5中,对我来说无法工作。虽然编译通过,但在运行时发生错误:Dictionary<String, Any>不符合可解码协议,因为Any不符合可解码协议。 - zoul
我刚刚发现了你提到的空JSON的问题,并更新了我的新解决方案。抱歉我之前漏掉了这个情况。 - Pitiphong Phongpattranont
显示剩余4条评论

2

2

详情

  • Xcode 12.0.1 (12A7300)
  • Swift 5.3

基于Tai Le

// 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

1
这是一个更通用的(不仅限于[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个属性足以进行自动解码,可能还有3个需要一些定制解码处理? - Michał Ziobro
@MichałZiobro 您想将部分数据解码为JSON对象,将另一部分解码为单独的实例变量吗?还是您正在询问是否仅针对对象的一部分编写部分解码初始化程序(它与JSON结构没有任何共同之处)?据我所知,第一个问题的答案是肯定的,第二个问题的答案是否定的。 - Alexey Kozhevnikov
我希望只有一些属性进行定制解码,其余的使用标准默认解码。 - Michał Ziobro
如果我理解正确的话,这是不可能的。无论如何,你的问题与当前的SO问题无关,值得单独提出一个问题。 - Alexey Kozhevnikov

1
我在这个主题上使用了一些答案,以获取对我来说最简单的解决方案。我的问题是我接收到了一个 [String: Any] 类型的字典,但我可以很好地使用一个 [String: String],将其他Any值转换为String。所以这是我的解决方案:
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]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接