使用字典/数组初始化一个符合Codable协议的对象

22

我的主要用例是使用字典创建一个对象,例如:

struct Person: Codable { let name: String }    
let dictionary = ["name": "Bob"]
let person = Person(from: dictionary)    

我希望避免编写自定义实现,并尽可能高效。

3个回答

55

目前我拥有的最佳解决方案是这个,但它需要编码/解码的额外开销。

extension Decodable {
  init(from: Any) throws {
    let data = try JSONSerialization.data(withJSONObject: from, options: .prettyPrinted)
    let decoder = JSONDecoder()
    self = try decoder.decode(Self.self, from: data)
  }
}

根据问题中的例子,结果将会是:

let person = Person(from: dictionary)

如果你想要倒过来做,这可能会有所帮助:https://dev59.com/5FcO5IYBdhLWcg3wzEqf#46329055


3
日期格式化程序部分是用来做什么的? - Marty
@Marty:使用Codable,您可以在解码器中定义自己的日期格式,以便正确设置对象的日期属性。 - smukamuka
@smukamuka 是的,但在这种特定情况下,它与问题有什么关系呢...? :) - Marty
1
什么都没有!只是我的特定问题中有一个日期,而json序列化会自动编码日期并且可解码的方式一开始让我感到困惑,所以我先将其保留。 - Chris Mitchelmore
1
这是一个很好的答案。如果您来自 https://dev59.com/5FcO5IYBdhLWcg3wzEqf#46329055 ,则应删除日期格式化程序行,因为它们会破坏该用例中的解码。 - lewis
我相信.prettyPrinted只是增加了一些不必要的开销,但它的影响应该是可以忽略不计的,因为处理JSON才是主要的性能瓶颈。 - Alex Cohn

4
基于 Chris Mitchelmore答案

细节

  • Xcode 14
  • Swift 5.6.1

解决方案

import Foundation

enum DictionaryParsingError: Error {
    case jsonSerialization(Error)
    case decode(Error)
}

extension Decodable {

    static func from<Key>(dictionary: [Key: Any],
                          options: JSONSerialization.WritingOptions = [],
                          decoder: JSONDecoder) -> Result<Self, DictionaryParsingError> where Key: Hashable {
        let data: Data
        do {
            data = try JSONSerialization.data(withJSONObject: dictionary, options: options)
        } catch let error {
            return .failure(.jsonSerialization(error))
        }

        do {
            return .success(try decoder.decode(Self.self, from: data))
        } catch let error {
            return .failure(.decode(error))
        }
    }

    static func from<Key>(dictionary: [Key: Any],
                          options: JSONSerialization.WritingOptions = [],
                          singleUseDecoder configuration: (JSONDecoder) -> ()) -> Result<Self, DictionaryParsingError> where Key: Hashable {
        let decoder = JSONDecoder()
        configuration(decoder)
        return from(dictionary: dictionary, options: options, decoder: decoder)
    }
}

使用方法

struct Item: Decodable {
    let id: Int
    let name: String
    var date: Date
    let isActive: Bool
}

let dictionary = ["id": 1,
                  "name": "Item",
                  "date": "2019-08-06T06:55:00.000-04:00",
                  "is_active": false] as [String : Any]

print("========================")
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
switch Item.from(dictionary: dictionary, decoder: decoder) {
case let .failure(error): print("ERROR: \(error)")
case let .success(item): print(item)
}

print("\n========================")
let item2 = Item.from(dictionary: dictionary) { decoder in
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
}
switch item2 {
case let .failure(error): print("ERROR: \(error)")
case let .success(item): print(item)
}

print("\n========================")
let item3 = Item.from(dictionary: [String:Any]()) { decoder in
    decoder.keyDecodingStrategy = .convertFromSnakeCase
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
            decoder.dateDecodingStrategy = .formatted(dateFormatter)
}
switch item3 {
case let .failure(error): print("ERROR: \(error)")
case let .success(item): print(item)
}

使用日志
========================
Item(id: 1, name: "Item", date: 2019-08-06 10:55:00 +0000, isActive: false)

========================
Item(id: 1, name: "Item", date: 2019-08-06 10:55:00 +0000, isActive: false)

========================
ERROR: decode(Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil)))

2

我改编了Chris Mitchelmore的答案,使其成为可失败的初始化器,而不是抛出代码。在某些情况下更加实用。

extension Decodable {
    init?(from: Any) {
        guard let data = try? JSONSerialization.data(withJSONObject: from, options: .prettyPrinted) else { return nil }
        let decoder = JSONDecoder()
        guard let decoded = try? decoder.decode(Self.self, from: data) else { return nil }
        self = decoded
    }
}

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