Andrei F
Andrei F

Reputation: 4394

Passing types dynamically to JSONDecoder

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?

Upvotes: 0

Views: 548

Answers (1)

ekscrypto
ekscrypto

Reputation: 3816

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 <T> will be of type GetUnreadCountsResponse at runtime.

Upvotes: 1

Related Questions