如何使用Swift的Codable将数据编码为字典?

171

我有一个实现了Swift 4的Codable协议的结构体。是否有一种简单的内置方法将该结构体编码为字典?

let struct = Foo(a: 1, b: 2)
let dict = something(struct)
// now dict is ["a": 1, "b": 2]

https://dev59.com/alYN5IYBdhLWcg3w-cfx#46597941 - Leo Dabus
16个回答

357

如果你不介意移动一些数据,你可以使用类似这样的方法:

extension Encodable {
  func asDictionary() throws -> [String: Any] {
    let data = try JSONEncoder().encode(self)
    guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
      throw NSError()
    }
    return dictionary
  }
}

或者是可选的变体

extension Encodable {
  var dictionary: [String: Any]? {
    guard let data = try? JSONEncoder().encode(self) else { return nil }
    return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
  }
}

假设Foo符合Codable或真正的Encodable,那么你可以这样做。

Assuming Foo conforms to Codable or really Encodable then you can do this.
let struct = Foo(a: 1, b: 2)
let dict = try struct.asDictionary()
let optionalDict = struct.dictionary

如果你想使用(init(any))这种方式进行转换,请参考这篇文章:Init an object conforming to Codable with a dictionary/array


1
可选变量的实现很棒,干净、流畅,非常适合 guard let 语句。真的可以使 API 调用更加简洁明了。 - Iron John Bonney
6
编码成数据后再从数据中解码,如果解码的数据块很大,性能惩罚将会很明显。 - DawnSong
1
用户使用 .flatMap() 是否有点多余?你可以移除那部分代码,只在结尾处使用可选项转换,如 as? [String: Any] - Eric

41
这里提供了简单的DictionaryEncoder / DictionaryDecoder 实现,它们包装了JSONEncoderJSONDecoderJSONSerialization,同时处理编码/解码策略...
class DictionaryEncoder {

    private let encoder = JSONEncoder()

    var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy {
        set { encoder.dateEncodingStrategy = newValue }
        get { return encoder.dateEncodingStrategy }
    }

    var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy {
        set { encoder.dataEncodingStrategy = newValue }
        get { return encoder.dataEncodingStrategy }
    }

    var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy {
        set { encoder.nonConformingFloatEncodingStrategy = newValue }
        get { return encoder.nonConformingFloatEncodingStrategy }
    }

    var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy {
        set { encoder.keyEncodingStrategy = newValue }
        get { return encoder.keyEncodingStrategy }
    }

    func encode<T>(_ value: T) throws -> [String: Any] where T : Encodable {
        let data = try encoder.encode(value)
        return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
    }
}

class DictionaryDecoder {

    private let decoder = JSONDecoder()

    var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy {
        set { decoder.dateDecodingStrategy = newValue }
        get { return decoder.dateDecodingStrategy }
    }

    var dataDecodingStrategy: JSONDecoder.DataDecodingStrategy {
        set { decoder.dataDecodingStrategy = newValue }
        get { return decoder.dataDecodingStrategy }
    }

    var nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy {
        set { decoder.nonConformingFloatDecodingStrategy = newValue }
        get { return decoder.nonConformingFloatDecodingStrategy }
    }

    var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy {
        set { decoder.keyDecodingStrategy = newValue }
        get { return decoder.keyDecodingStrategy }
    }

    func decode<T>(_ type: T.Type, from dictionary: [String: Any]) throws -> T where T : Decodable {
        let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
        return try decoder.decode(type, from: data)
    }
}

使用方法类似于JSONEncoder/JSONDecoder...

let dictionary = try DictionaryEncoder().encode(object)

并且

let object = try DictionaryDecoder().decode(Object.self, from: dictionary)

为了方便起见,我将所有内容放在了一个存储库中...https://github.com/ashleymills/SwiftDictionaryCoding

非常感谢!另一种方法是使用继承,但调用站点将无法推断类型为字典,因为存在两个返回类型不同的函数。 - user1046037
1
比被接受的答案好得多。+1 - Pranav Kasetti

18

我创建了一个名为CodableFirebase的库,它最初的目的是与Firebase数据库一起使用,但实际上它能够满足您的需求:它创建了一个字典或者其他类型,就像在JSONDecoder中一样,但您不需要像其他答案中那样进行双重转换。因此,它看起来应该是这样的:

import CodableFirebase

let model = Foo(a: 1, b: 2)
let dict = try! FirebaseEncoder().encode(model)

12

没有内置的方法可以做到这一点。

正如上面回答的,如果您没有性能问题,则可以接受 JSONEncoder + JSONSerialization 的实现。

但我宁愿使用标准库提供的编码器/解码器对象。

class DictionaryEncoder {
    private let jsonEncoder = JSONEncoder()

    /// Encodes given Encodable value into an array or dictionary
    func encode<T>(_ value: T) throws -> Any where T: Encodable {
        let jsonData = try jsonEncoder.encode(value)
        return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
    }
}

class DictionaryDecoder {
    private let jsonDecoder = JSONDecoder()

    /// Decodes given Decodable type from given array or dictionary
    func decode<T>(_ type: T.Type, from json: Any) throws -> T where T: Decodable {
        let jsonData = try JSONSerialization.data(withJSONObject: json, options: [])
        return try jsonDecoder.decode(type, from: jsonData)
    }
}

您可以尝试以下代码:

您可以使用以下代码进行尝试:

struct Computer: Codable {
    var owner: String?
    var cpuCores: Int
    var ram: Double
}

let computer = Computer(owner: "5keeve", cpuCores: 8, ram: 4)
let dictionary = try! DictionaryEncoder().encode(computer)
let decodedComputer = try! DictionaryDecoder().decode(Computer.self, from: dictionary)

我在这里强行试图缩短示例。在生产代码中,您应该适当地处理错误。


7

let dict = try JSONSerialization.jsonObject(with: try JSONEncoder().encode(struct), options: []) as? [String: Any]

可以翻译为:

尝试将结构体编码为JSON,使用选项[],然后尝试将其反序列化为[String:Any]字典。


6

我不确定这是否是最好的方法,但你肯定可以像这样做:

struct Foo: Codable {
    var a: Int
    var b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }
}

let foo = Foo(a: 1, b: 2)
let dict = try JSONDecoder().decode([String: Int].self, from: JSONEncoder().encode(foo))
print(dict)

8
这仅适用于所有属性种类相同的结构。 - Leo Dabus
1
我刚试了一下 "let dict = try JSONDecoder().decode([String: Int].self, from: JSONEncoder().encode(foo))",但是出现了 "Expected to decode Dictionary<String, Any> but found an array instead." 的错误。请问能帮忙解决吗? - Am1rFT

5
我将Swift项目中的PropertyListEncoder修改为DictionaryEncoder,只需将字典序列化成二进制格式即可。您可以自己实现相同的功能,或者从这里获取我的代码。
可以按照以下方式使用:
do {
    let employeeDictionary: [String: Any] = try DictionaryEncoder().encode(employee)
} catch let error {
    // handle error
}

4

在一些项目中,我使用了Swift反射。但是要小心,嵌套的可编码对象也没有被映射。

let dict = Dictionary(uniqueKeysWithValues: Mirror(reflecting: foo).children.map{ ($0.label!, $0.value) })

3
我认为仅使用 Codable 将数据编码到/从字典中是有价值的,而不必考虑 JSON/Plists 或其他格式。许多 API 只返回一个字典或者期望一个字典,这时轻松地用 Swift 结构体或对象进行交换非常方便,避免了大量重复枯燥的代码编写。
我基于 Foundation 中的 JSONEncoder.swift 实现了一些代码(实际上在内部执行字典编码/解码,但没有导出),并对其进行了扩展。可以在此处找到该代码:https://github.com/elegantchaos/DictionaryCoding
目前代码还比较简单,但我已经对其进行了一些扩展,例如在解码时可以填充缺失的值。

3

这里有一个基于协议的解决方案:

protocol DictionaryEncodable {
    func encode() throws -> Any
}

extension DictionaryEncodable where Self: Encodable {
    func encode() throws -> Any {
        let jsonData = try JSONEncoder().encode(self)
        return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
    }
}

protocol DictionaryDecodable {
    static func decode(_ dictionary: Any) throws -> Self
}

extension DictionaryDecodable where Self: Decodable {
    static func decode(_ dictionary: Any) throws -> Self {
        let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
        return try JSONDecoder().decode(Self.self, from: jsonData)
    }
}

typealias DictionaryCodable = DictionaryEncodable & DictionaryDecodable

以下是如何使用它:

class AClass: Codable, DictionaryCodable {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

struct AStruct: Codable, DictionaryEncodable, DictionaryDecodable {
    
    var name: String
    var age: Int
}

let aClass = AClass(name: "Max", age: 24)

if let dict = try? aClass.encode(), let theClass = try? AClass.decode(dict) {
    print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theClass.name), age: \(theClass.age)\"")
}

let aStruct = AStruct(name: "George", age: 30)

if let dict = try? aStruct.encode(), let theStruct = try? AStruct.decode(dict) {
    print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theStruct.name), age: \(theStruct.age)\"")
}

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