【问题标题】:How to use Swift JSONDecode with dynamic types?如何将 Swift JSONDecode 与动态类型一起使用?
【发布时间】:2017-11-24 11:18:39
【问题描述】:

我的应用程序有一个本地缓存,并从/向服务器发送/接收模型。所以我决定构建一个地图 [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$10", contact)

我想这样解码:

let result = try! decoder.decode(modelNameToType["ContactModel"]!, from: data)

但我得到了错误:

不能使用类型参数列表调用“解码”(Codable.Type, 来自:数据)

我做错了什么?任何帮助表示赞赏

修复类型有效,并解决任何本地请求,但不是远程请求。

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

...如果我解决了解码问题。任何帮助表示赞赏。

【问题讨论】:

  • 这个“decoder.decode(ContactModel.self, from: data)”怎么还是不行?这个看起来是对的。请同时发布您的 ContactModel 类定义。
  • 我只是说 decoder.decode(ContactModel.self, from: data) 确实有效!
  • 当您说“动态”时,您希望result 是什么类型? (它必须是编译时的特定类型。它不能是“在运行时的任何类型。”如果它是“一些在编译时未知的随机类型”你可以调用什么方法换句话说,使用result 的下一行代码是什么?)
  • 这个应用程序有一个本地缓存,并且经常接收和发送信息到服务器。我将按照您的建议更新问题以具有整个功能
  • 这与question密切相关

标签: json swift decode encode


【解决方案1】:

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 doesn't conform to itself

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 = $0.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's

func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T

T 是协议类型时,type: 参数采用.Protocol 元类型,而不是.Type 元类型(有关详细信息,请参阅this Q&A)。

我们现在可以为我们的键定义一个类型,带有一个modelType 属性,它返回一个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 对象,并使用关键路径与缓存进行交互layer),但不知道更多关于CRUD.shared 以及你如何使用它的信息;很难说。

【讨论】:

  • 谢谢哈米什。很好理解和回答。我没有看到这种语言行为的出现,但我承认我在 Swift 方面的经验不如其他语言。
【解决方案2】:

我想这样解码:

let result = try! decoder.decode(modelNameToType["ContactModel"]!, from: data)

但我得到了错误:

Cannot invoke 'decode' with an argument list of type (Codable.Type, from: Data)

您错误地使用了decodedecoder.decode 的第一个参数不能是object;它必须是一个类型。您不能传递包含在表达式中的元类型。

但是,您可以传递一个 object 并获取它的类型。所以你可以用一个保证我们是可解码采用者的泛型来解决这个问题。这是一个最小的例子:

func testing<T:Decodable>(_ t:T, _ data:Data) {
    let result = try! JSONDecoder().decode(type(of:t), from: data)
    // ...
}

如果您将 ContactModel 实例作为第一个参数传递,那是合法的。

【讨论】:

  • 修改了我的回答,建议使用泛型和type(of:)
  • 嘿,马特,在这种情况下,我需要在字典 [string:any] 上存储一个对象。但是当我做 type(of: dictionary[key]) 我得到任何?结果。
  • @RodrigoFava 所以打开它! type(of: dictionary[key]!)(除非您应该安全地打开包装。) Optional Any 的类型是 Optional Any。但是打开 Optional,你有一个 Any,现在 type(of:) 正确地采用了底层类型。
  • 对于每种模型类型,我们都将花费 +1 个实例...可以通过闭包来完成吗?
  • 我确实实现了我的替代方案,并且我正在借此机会了解有关该语言的更多信息。没有冒犯的意思,还是谢谢你。我会给你答案的功劳。
【解决方案3】:

这是一个类似于@Hamish 的AnyCodable 解决方案的解决方案,它需要的工作更少,但只适用于类。

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 的扩展中。我们对EncodableKeyedEncodingContainer.encode 重复相同的过程以获得编码功能。

现在,[String: AnyDynamicCodable] 符合 Codable

如果您想使用这种方法但使用结构,请考虑使用轻量级类包装器,例如

final class DynamicCodableWrapper<T: Codable>: Codable {
    let value: T
    init(_ value: T) {
        self.value = value
    }
}

您甚至可能希望将其构建到您的 AnyDynamicCodable 类型中以创建某种 AnyCodable 类型。

【讨论】:

    【解决方案4】:

    我相信这就是您正在寻找的解决方案。

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

    【讨论】:

    • Astralis,我会更新我的问题,看看是否可以更清楚地说明情况
    【解决方案5】:

    您可以考虑的一种方法是定义两个不同的结构,每个结构都有不同的数据类型用于更改的字段。如果第一次解码失败,则尝试使用第二种数据类型进行解码,如下所示:

    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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-02-03
      • 2021-07-28
      • 1970-01-01
      • 2020-10-20
      • 2015-03-07
      • 2021-02-15
      • 2021-10-22
      • 1970-01-01
      相关资源
      最近更新 更多