0

I am trying to decode from json objects with generic nested object, and for this I want to pass the type of class dynamically when decoding.

For example, my classes are EContactModel and ENotificationModel which extend ObjectModel (and :Codable)s. ENotificationModel can contain a nested ObjectModel (which can be a contact, notification or other objectmodel).

I have a dictionary of types like this:

static let OBJECT_STRING_CLASS_MAP = [
        "EContactModel" : EContactModel.self,
        "ENotificationModel" : ENotificationModel.self
...
    ]

My decoding init method in ENotificationModel looks like this:

required init(from decoder: Decoder) throws
    {
        try super.init(from: decoder)
        let values = try decoder.container(keyedBy: CodingKeys.self)


    ...
    //decode some fields here
    self.message = try values.decodeIfPresent(String.self, forKey: .message)
    ...

    //decode field "masterObject" of generic type ObjectModel
    let cls = ObjectModelTypes.OBJECT_STRING_CLASS_MAP[classNameString]!
    let t = type(of: cls)
    print(cls) //this prints "EContactModel"
    self.masterObject = try values.decodeIfPresent(cls, forKey: .masterObject)
    print(t) //prints ObjectModel.Type
    print(type(of: self.masterObject!)) //prints ObjectModel

}

I also tried passing type(of: anObjectInstanceFromADictionary) and still not working, but if I pass type(of: EContactModel()) it works. I cannot understand this, because both objects are the same (ie. instance of EContactModel)

Is there a solution for this?

Andrei F
  • 4,205
  • 9
  • 35
  • 66

1 Answers1

1

You could declare your object models with optional variables and let JSONDecoder figure it out for you.

class ApiModelImage: Decodable {
    let file: String
    let thumbnail_file: String
    ...
}

class ApiModelVideo: Decodable {
    let thumbnail: URL
    let duration: String?
    let youtube_id: String
    let youtube_url: URL
    ...
}

class ApiModelMessage: Decodable {
    let title: String
    let body: String
    let image: ApiModelImage?
    let video: ApiModelVideo?
    ...
}

Then all you have to do is....

if let message = try? JSONDecoder().decode(ApiModelMessage.self, from: data) {
    if let image = message.image {
        print("yay, my message contains an image!")
    }
    if let video = message.video {
        print("yay, my message contains a video!")
    }
}

Alternatively, you could use generics and specify the type when calling your API code:

func get<T: Decodable>(from endpoint: String, onError: @escaping(_: Error?) -> Void, onSuccess: @escaping (_: T) -> Void) {
    getData(from: endpoint, onError: onError) { (data) in
        do {
            let response = try JSONDecoder().decode(T.self, from: data)
            onSuccess(response)
        } catch {
            onError(error)
        }
    }
}

Used this way, you just have to make sure you define your expected response type:

    let successCb = { (_ response: GetUnreadCountsResponse) in
        ...
    }
    ApiRequest().get(from: endpoint, onError: { (_) in 
        ...
    }, onSuccess: successCb)

Since you define successCb as requiring a GetUnreadCountsResponse model, the API get method generic will be of type GetUnreadCountsResponse at runtime.

Good Luck!

ekscrypto
  • 3,718
  • 1
  • 24
  • 38
  • Ok, but this is not my case. In my model I have a lot of entities that extend ObjectModel. So my field types are generic. I cannot have a field for each object type. In your example, what i need is a field, let's say "media" of type Decodable (or a type that extends Decodable), then decode that field dynamically without switch/if – Andrei F Jul 27 '18 at 12:18
  • Thank you for the answer. The problem is that when trying to apply the same logic in the init(from: decoder) method, the type returned is of that of the superclass, not the actual class. – Andrei F Jul 27 '18 at 14:01