How to use Swift JSONDecode with dynamic types?

前提是你 提交于 2019-12-09 12:12:19

问题


My App has a local cache and sends/receives models from/to the server. So I decided to build a map [String : Codable.Type], essentially to be able to decode anything I have on this generic cache either created locally or received from server.

let encoder = JSONEncoder()
let decoder = JSONDecoder()
var modelNameToType = [String : Codable.Type]()
modelNameToType = ["ContactModel": ContactModel.Self, "AnythingModel" : AnythingModel.Self, ...] 

Whatever I create on the App I can encode successfully and store on cache like this:

let contact = ContactModel(name: "John")
let data = try! encoder.encode(contact)
CRUD.shared.storekey(key: "ContactModel$10", contact)

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)

What am I doing wrong? Any help is appreciated

Fixing the type works, and solves any local request, but not a remote request.

let result = try! decoder.decode(ContactModel.self, from: data)

Contact Model:

struct ContactModel: Codable {
    var name : String
}

For remote requests I would have a function like this:

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

...if I solve the decode issue. Any help appreciated.


回答1:


The Codable API is built around encoding from and decoding into concrete types. However, the round-tripping you want here shouldn't have to know about any concrete types; it's merely concatenating heterogenous JSON values into a JSON object.

Therefore, JSONSerialization is a better tool for the job in this case, as it deals with 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)
}

That being said, you could still make this with JSONDecoder/JSONEncoder – however it requires quite a bit of type-erasing boilerplate.

For example, we need a wrapper type that conforms to Encodable, as 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)
  }
}

We also need a wrapper to capture a concrete type that can be used for decoding:

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

We cannot simply pass a Decodable.Type to JSONDecoder's

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

as when T is a protocol type, the type: parameter takes a .Protocol metatype, not a .Type metatype (see this Q&A for more info).

We can now define a type for our keys, with a modelType property that returns an AnyCodableType that we can use for decoding 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)
    }
  }
}

and then do something like this for the round-tripping:

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

This probably could be designed better to work with Codable rather than against it (perhaps a struct to represent the JSON object you send to the server, and use key paths to interact with the caching layer), but without knowing more about CRUD.shared and how you use it; it's hard to say.




回答2:


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)

You are using decode incorrectly. The first parameter to decoder.decode must not be an object; it must be a type. You cannot pass a metatype wrapped up in an expression.

You can, however, pass an object and take its type. So you could solve this with a generic that guarantees that we are a Decodable adopter. Here's a minimal example:

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

If you pass a ContactModel instance as the first parameter, that's legal.




回答3:


I believe this is the solution you are looking for.

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)

This will print the following.

ContactModel(name: "John")



回答4:


One approach you can consider is defining two different structs, each with a different data type for the field that changes. If the first decode fails, then try decoding with the second data type like this:

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
}


来源:https://stackoverflow.com/questions/47472197/how-to-use-swift-jsondecode-with-dynamic-types

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!