如何将 Swift JSONDecode 与动态类型一起使用?
How to use Swift JSONDecode with dynamic types?
我的应用程序有一个本地缓存和 sends/receives 模型 from/to 服务器。所以我决定构建一个映射 [String : Codable.Type],主要是为了能够解码我在这个通用缓存上拥有的任何内容,无论是本地创建的还是从服务器接收的。
let encoder = JSONEncoder()
let decoder = JSONDecoder()
var modelNameToType = [String : Codable.Type]()
modelNameToType = ["ContactModel": ContactModel.Self, "AnythingModel" : AnythingModel.Self, ...]
无论我在应用程序上创建什么,我都可以成功编码并存储在缓存中,如下所示:
let contact = ContactModel(name: "John")
let data = try! encoder.encode(contact)
CRUD.shared.storekey(key: "ContactModel", contact)
我想这样解码:
let result = try! decoder.decode(modelNameToType["ContactModel"]!, from: data)
但我收到错误:
Cannot invoke 'decode' with an argument list of type (Codable.Type,
from: Data)
我做错了什么?感谢任何帮助
修复类型有效,并解决了任何本地请求,但不是远程请求。
let result = try! decoder.decode(ContactModel.self, from: data)
联系方式:
struct ContactModel: Codable {
var name : String
}
对于远程请求,我会有这样的函数:
func buildAnswer(keys: [String]) -> Data {
var result = [String:Codable]()
for key in keys {
let data = CRUD.shared.restoreKey(key: key)
let item = try decoder.decode(modelNameToType[key]!, from: data)
result[key] = item
}
return try encoder.encode(result)
}
...如果我解决了解码问题。任何帮助表示赞赏。
我相信这就是您正在寻找的解决方案。
import Foundation
struct ContactModel: Codable {
let name: String
}
let encoder = JSONEncoder()
let decoder = JSONDecoder()
var map = [String: Codable]()
map["contact"] = ContactModel(name: "John")
let data = try! encoder.encode(map["contact"] as! ContactModel)
let result = try! decoder.decode(ContactModel.self, from: data)
debugPrint(result)
这将打印以下内容。
ContactModel(name: "John")
I would like to decode like this:
let result = try! decoder.decode(modelNameToType["ContactModel"]!, from: data)
But I get the error:
Cannot invoke 'decode' with an argument list of type (Codable.Type, from: Data)
您使用的 decode
不正确。 decoder.decode
的第一个参数不能是 object;它必须是 类型 。您不能传递包含在表达式中的元类型。
但是,您可以传递一个 对象 并获取其类型。所以你可以用一个保证我们是可解码采用者的泛型来解决这个问题。这是一个最小的例子:
func testing<T:Decodable>(_ t:T, _ data:Data) {
let result = try! JSONDecoder().decode(type(of:t), from: data)
// ...
}
如果您将 ContactModel 实例作为第一个参数传递,那是合法的。
Codable
API 是围绕从 具体 类型编码和解码而构建的。但是,您在这里想要的往返不必知道任何具体类型;它只是将异构 JSON 值连接到一个 JSON 对象中。
因此,在这种情况下,JSONSerialization
是一个更好的工具,因为它处理 Any
:
import Foundation
// I would consider lifting your String keys into their own type btw.
func buildAnswer(keys: [String]) throws -> Data {
var result = [String: Any](minimumCapacity: keys.count)
for key in keys {
let data = CRUD.shared.restoreKey(key: key)
result[key] = try JSONSerialization.jsonObject(with: data)
}
return try JSONSerialization.data(withJSONObject: result)
}
也就是说,你 可以 仍然使用 JSONDecoder
/JSONEncoder
来实现——但是它需要相当多的类型擦除样板文件。
例如,我们需要一个符合Encodable
的包装器类型,如Encodable
:
import Foundation
struct AnyCodable : Encodable {
private let _encode: (Encoder) throws -> Void
let base: Codable
let codableType: AnyCodableType
init<Base : Codable>(_ base: Base) {
self.base = base
self._encode = {
var container = [=11=].singleValueContainer()
try container.encode(base)
}
self.codableType = AnyCodableType(type(of: base))
}
func encode(to encoder: Encoder) throws {
try _encode(encoder)
}
}
我们还需要一个包装器来捕获可用于解码的具体类型:
struct AnyCodableType {
private let _decodeJSON: (JSONDecoder, Data) throws -> AnyCodable
// repeat for other decoders...
// (unfortunately I don't believe there's an easy way to make this generic)
//
let base: Codable.Type
init<Base : Codable>(_ base: Base.Type) {
self.base = base
self._decodeJSON = { decoder, data in
AnyCodable(try decoder.decode(base, from: data))
}
}
func decode(from decoder: JSONDecoder, data: Data) throws -> AnyCodable {
return try _decodeJSON(decoder, data)
}
}
我们不能简单地将 Decodable.Type
传递给 JSONDecoder
的
func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T
当 T
是协议类型时,type:
参数采用 .Protocol
元类型,而不是 .Type
元类型(有关更多信息,请参见 信息)。
我们现在可以为我们的密钥定义一个类型,其中 modelType
属性 returns 和 AnyCodableType
我们可以用于解码 JSON :
enum ModelName : String {
case contactModel = "ContactModel"
case anythingModel = "AnythingModel"
var modelType: AnyCodableType {
switch self {
case .contactModel:
return AnyCodableType(ContactModel.self)
case .anythingModel:
return AnyCodableType(AnythingModel.self)
}
}
}
然后为往返做这样的事情:
func buildAnswer(keys: [ModelName]) throws -> Data {
let decoder = JSONDecoder()
let encoder = JSONEncoder()
var result = [String: AnyCodable](minimumCapacity: keys.count)
for key in keys {
let rawValue = key.rawValue
let data = CRUD.shared.restoreKey(key: rawValue)
result[rawValue] = try key.modelType.decode(from: decoder, data: data)
}
return try encoder.encode(result)
}
这可能设计得更好与Codable
一起工作而不是反对它(也许是一个结构来表示您发送到的JSON对象服务器,并使用关键路径与缓存层交互),但不了解 CRUD.shared
以及如何使用它;很难说。
您可以考虑的一种方法是定义两个不同的结构,每个结构都为发生变化的字段使用不同的数据类型。如果第一次解码失败,则尝试使用第二种数据类型进行解码,如下所示:
struct MyDataType1: Decodable {
let user_id: String
}
struct MyDataType2: Decodable {
let user_id: Int
}
do {
let myDataStruct = try JSONDecoder().decode(MyDataType1.self, from: jsonData)
} catch let error {
// look at error here to verify it is a type mismatch
// then try decoding again with type MyDataType2
}
这是一个类似于@Hamish 的 AnyCodable
解决方案的解决方案,它需要较少的工作但只适用于 classes。
typealias DynamicCodable = AnyObject & Codable
extension Decodable {
static func decode<K: CodingKey>(from container: KeyedDecodingContainer<K>, forKey key: K) throws -> Self {
try container.decode(Self.self, forKey: key)
}
}
extension Encodable {
func encode<K: CodingKey>(to container: inout KeyedEncodingContainer<K>, forKey key: K) throws {
try container.encode(self, forKey: key)
}
}
struct AnyDynamicCodable: Codable {
let value: DynamicCodable
init(_ value: DynamicCodable) {
self.value = value
}
enum CodingKeys: String, CodingKey {
case type
case value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeName = try container.decode(String.self, forKey: .type)
guard let type = NSClassFromString(typeName) as? DynamicCodable.Type else {
throw DecodingError.typeMismatch(DynamicCodable.Type.self, .init(codingPath: decoder.codingPath + [CodingKeys.type], debugDescription: "NSClassFromString returned nil or did not conform to DynamicCodable.", underlyingError: nil))
}
self.value = try type.decode(from: container, forKey: .value)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let typeName: String = NSStringFromClass(type(of: self.value))
try container.encode(typeName, forKey: .type)
try self.value.encode(to: &container, forKey: .value)
}
}
这依赖于一些功能。
首先,如果一个值是一个对象,那么它的类型可以用 NSStringFromClass
转换为 String
,然后用 NSClassFromString
恢复。老实说,我不确定这有多安全,但在我所做的有限测试中,这种方法似乎对我有用。我在一个单独的问题 here 中问过这个问题。
这让我们可以编码和解码对象的类型。
一旦我们有了一个类型,我们希望能够像这样解码这个类型的值:
let value = try container.decode(type, forKey .value)
但这是不可能的。原因是 KeyedDecodingContainer.decode
是一个泛型函数,其类型参数必须在编译时已知。这导致我们
extension Decodable {
static func decode<K: CodingKey>(from container: KeyedDecodingContainer<K>, forKey key: K) throws -> Self {
try container.decode(Self.self, forKey: key)
}
}
然后我们可以写
let value = try type.decode(from: container, forKey: .value)
相反。
这是一个非常通用的技术。如果您需要将动态(运行时)类型插入泛型函数,请将其包装在泛型函数类型约束的扩展中。不幸的是,您将无法使用包装采用 Any
的方法,因为您无法扩展 Any
。在这种情况下,KeyedDecodingContainer.decode
的类型参数是 Decodable
,因此我们将此方法包装在 Decodable
的扩展中。我们对 Encodable
和 KeyedEncodingContainer.encode
重复相同的过程以获得编码功能。
现在,[String: AnyDynamicCodable]
符合 Codable
。
如果你想使用这种方法但使用结构,请考虑使用轻量级 class 包装器,例如
final class DynamicCodableWrapper<T: Codable>: Codable {
let value: T
init(_ value: T) {
self.value = value
}
}
您甚至可能希望将其构建到您的 AnyDynamicCodable
类型中以制作某种 AnyCodable
类型。
我的应用程序有一个本地缓存和 sends/receives 模型 from/to 服务器。所以我决定构建一个映射 [String : Codable.Type],主要是为了能够解码我在这个通用缓存上拥有的任何内容,无论是本地创建的还是从服务器接收的。
let encoder = JSONEncoder()
let decoder = JSONDecoder()
var modelNameToType = [String : Codable.Type]()
modelNameToType = ["ContactModel": ContactModel.Self, "AnythingModel" : AnythingModel.Self, ...]
无论我在应用程序上创建什么,我都可以成功编码并存储在缓存中,如下所示:
let contact = ContactModel(name: "John")
let data = try! encoder.encode(contact)
CRUD.shared.storekey(key: "ContactModel", contact)
我想这样解码:
let result = try! decoder.decode(modelNameToType["ContactModel"]!, from: data)
但我收到错误:
Cannot invoke 'decode' with an argument list of type (Codable.Type, from: Data)
我做错了什么?感谢任何帮助
修复类型有效,并解决了任何本地请求,但不是远程请求。
let result = try! decoder.decode(ContactModel.self, from: data)
联系方式:
struct ContactModel: Codable {
var name : String
}
对于远程请求,我会有这样的函数:
func buildAnswer(keys: [String]) -> Data {
var result = [String:Codable]()
for key in keys {
let data = CRUD.shared.restoreKey(key: key)
let item = try decoder.decode(modelNameToType[key]!, from: data)
result[key] = item
}
return try encoder.encode(result)
}
...如果我解决了解码问题。任何帮助表示赞赏。
我相信这就是您正在寻找的解决方案。
import Foundation
struct ContactModel: Codable {
let name: String
}
let encoder = JSONEncoder()
let decoder = JSONDecoder()
var map = [String: Codable]()
map["contact"] = ContactModel(name: "John")
let data = try! encoder.encode(map["contact"] as! ContactModel)
let result = try! decoder.decode(ContactModel.self, from: data)
debugPrint(result)
这将打印以下内容。
ContactModel(name: "John")
I would like to decode like this:
let result = try! decoder.decode(modelNameToType["ContactModel"]!, from: data)
But I get the error:
Cannot invoke 'decode' with an argument list of type (Codable.Type, from: Data)
您使用的 decode
不正确。 decoder.decode
的第一个参数不能是 object;它必须是 类型 。您不能传递包含在表达式中的元类型。
但是,您可以传递一个 对象 并获取其类型。所以你可以用一个保证我们是可解码采用者的泛型来解决这个问题。这是一个最小的例子:
func testing<T:Decodable>(_ t:T, _ data:Data) {
let result = try! JSONDecoder().decode(type(of:t), from: data)
// ...
}
如果您将 ContactModel 实例作为第一个参数传递,那是合法的。
Codable
API 是围绕从 具体 类型编码和解码而构建的。但是,您在这里想要的往返不必知道任何具体类型;它只是将异构 JSON 值连接到一个 JSON 对象中。
因此,在这种情况下,JSONSerialization
是一个更好的工具,因为它处理 Any
:
import Foundation
// I would consider lifting your String keys into their own type btw.
func buildAnswer(keys: [String]) throws -> Data {
var result = [String: Any](minimumCapacity: keys.count)
for key in keys {
let data = CRUD.shared.restoreKey(key: key)
result[key] = try JSONSerialization.jsonObject(with: data)
}
return try JSONSerialization.data(withJSONObject: result)
}
也就是说,你 可以 仍然使用 JSONDecoder
/JSONEncoder
来实现——但是它需要相当多的类型擦除样板文件。
例如,我们需要一个符合Encodable
的包装器类型,如Encodable
import Foundation
struct AnyCodable : Encodable {
private let _encode: (Encoder) throws -> Void
let base: Codable
let codableType: AnyCodableType
init<Base : Codable>(_ base: Base) {
self.base = base
self._encode = {
var container = [=11=].singleValueContainer()
try container.encode(base)
}
self.codableType = AnyCodableType(type(of: base))
}
func encode(to encoder: Encoder) throws {
try _encode(encoder)
}
}
我们还需要一个包装器来捕获可用于解码的具体类型:
struct AnyCodableType {
private let _decodeJSON: (JSONDecoder, Data) throws -> AnyCodable
// repeat for other decoders...
// (unfortunately I don't believe there's an easy way to make this generic)
//
let base: Codable.Type
init<Base : Codable>(_ base: Base.Type) {
self.base = base
self._decodeJSON = { decoder, data in
AnyCodable(try decoder.decode(base, from: data))
}
}
func decode(from decoder: JSONDecoder, data: Data) throws -> AnyCodable {
return try _decodeJSON(decoder, data)
}
}
我们不能简单地将 Decodable.Type
传递给 JSONDecoder
的
func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T
当 T
是协议类型时,type:
参数采用 .Protocol
元类型,而不是 .Type
元类型(有关更多信息,请参见
我们现在可以为我们的密钥定义一个类型,其中 modelType
属性 returns 和 AnyCodableType
我们可以用于解码 JSON :
enum ModelName : String {
case contactModel = "ContactModel"
case anythingModel = "AnythingModel"
var modelType: AnyCodableType {
switch self {
case .contactModel:
return AnyCodableType(ContactModel.self)
case .anythingModel:
return AnyCodableType(AnythingModel.self)
}
}
}
然后为往返做这样的事情:
func buildAnswer(keys: [ModelName]) throws -> Data {
let decoder = JSONDecoder()
let encoder = JSONEncoder()
var result = [String: AnyCodable](minimumCapacity: keys.count)
for key in keys {
let rawValue = key.rawValue
let data = CRUD.shared.restoreKey(key: rawValue)
result[rawValue] = try key.modelType.decode(from: decoder, data: data)
}
return try encoder.encode(result)
}
这可能设计得更好与Codable
一起工作而不是反对它(也许是一个结构来表示您发送到的JSON对象服务器,并使用关键路径与缓存层交互),但不了解 CRUD.shared
以及如何使用它;很难说。
您可以考虑的一种方法是定义两个不同的结构,每个结构都为发生变化的字段使用不同的数据类型。如果第一次解码失败,则尝试使用第二种数据类型进行解码,如下所示:
struct MyDataType1: Decodable {
let user_id: String
}
struct MyDataType2: Decodable {
let user_id: Int
}
do {
let myDataStruct = try JSONDecoder().decode(MyDataType1.self, from: jsonData)
} catch let error {
// look at error here to verify it is a type mismatch
// then try decoding again with type MyDataType2
}
这是一个类似于@Hamish 的 AnyCodable
解决方案的解决方案,它需要较少的工作但只适用于 classes。
typealias DynamicCodable = AnyObject & Codable
extension Decodable {
static func decode<K: CodingKey>(from container: KeyedDecodingContainer<K>, forKey key: K) throws -> Self {
try container.decode(Self.self, forKey: key)
}
}
extension Encodable {
func encode<K: CodingKey>(to container: inout KeyedEncodingContainer<K>, forKey key: K) throws {
try container.encode(self, forKey: key)
}
}
struct AnyDynamicCodable: Codable {
let value: DynamicCodable
init(_ value: DynamicCodable) {
self.value = value
}
enum CodingKeys: String, CodingKey {
case type
case value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeName = try container.decode(String.self, forKey: .type)
guard let type = NSClassFromString(typeName) as? DynamicCodable.Type else {
throw DecodingError.typeMismatch(DynamicCodable.Type.self, .init(codingPath: decoder.codingPath + [CodingKeys.type], debugDescription: "NSClassFromString returned nil or did not conform to DynamicCodable.", underlyingError: nil))
}
self.value = try type.decode(from: container, forKey: .value)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let typeName: String = NSStringFromClass(type(of: self.value))
try container.encode(typeName, forKey: .type)
try self.value.encode(to: &container, forKey: .value)
}
}
这依赖于一些功能。
首先,如果一个值是一个对象,那么它的类型可以用 NSStringFromClass
转换为 String
,然后用 NSClassFromString
恢复。老实说,我不确定这有多安全,但在我所做的有限测试中,这种方法似乎对我有用。我在一个单独的问题 here 中问过这个问题。
这让我们可以编码和解码对象的类型。
一旦我们有了一个类型,我们希望能够像这样解码这个类型的值:
let value = try container.decode(type, forKey .value)
但这是不可能的。原因是 KeyedDecodingContainer.decode
是一个泛型函数,其类型参数必须在编译时已知。这导致我们
extension Decodable {
static func decode<K: CodingKey>(from container: KeyedDecodingContainer<K>, forKey key: K) throws -> Self {
try container.decode(Self.self, forKey: key)
}
}
然后我们可以写
let value = try type.decode(from: container, forKey: .value)
相反。
这是一个非常通用的技术。如果您需要将动态(运行时)类型插入泛型函数,请将其包装在泛型函数类型约束的扩展中。不幸的是,您将无法使用包装采用 Any
的方法,因为您无法扩展 Any
。在这种情况下,KeyedDecodingContainer.decode
的类型参数是 Decodable
,因此我们将此方法包装在 Decodable
的扩展中。我们对 Encodable
和 KeyedEncodingContainer.encode
重复相同的过程以获得编码功能。
现在,[String: AnyDynamicCodable]
符合 Codable
。
如果你想使用这种方法但使用结构,请考虑使用轻量级 class 包装器,例如
final class DynamicCodableWrapper<T: Codable>: Codable {
let value: T
init(_ value: T) {
self.value = value
}
}
您甚至可能希望将其构建到您的 AnyDynamicCodable
类型中以制作某种 AnyCodable
类型。