I will try to explain what I want to do in a best possible way, just like I tried while Googling for the last couple of days.
My application is communicating with few different APIs, but let's consider responses from one API first. Response from each endpoint holds some 'common parameters', such as statuses or error messages, and a single object or array of objects that we're mostly interested in, it brings important data, we might want to encode it, store it, put it to Realm, CoreData, etc.
For Example, response with single object:
{
"status": "success",
"response_code": 200,
"messages": [
"message1",
"message2"
]
"data": {
OBJECT we're interested in.
}
}
Or, response with an array of objects:
{
"status": "success",
"response_code": 200,
"messages": [
"message1",
"message2"
]
"data": [
{
OBJECT we're interested in.
},
{
OBJECT we're interested in.
}
]
}
Okay. That is simple enough, easy to understand.
Now, I want to write a single "Root" Object that will hold 'common parameters', or status
, response_code
and messages
and have another, property for a specific object (or array of objects) that we're interested in.
Inheritance
First approach was creating a Root Object, like this:
class Root: Codable {
let status: String
let response_code: Int
let messages: [String]?
private enum CodingKeys: String, CodingKey {
case status, response_code, messages
}
required public init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self)
status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
messages = try container?.decodeIfPresent([String].self, forKey: .key)
}
public func encode(to encoder: Encoder) throws {}
}
Once I have this Root Object, I could create specific object that inherits from this Root object and pass my specific object in JSONDecoder, and there, I have a nice solution. But, this solution is failing for arrays. Maybe for someone it doesn't, but I can not stress enough how much I don't want to make additional 'plural' object that only exists to hold an array of Objects, like:
class Objects: Root {
let objects: [Object]
// Code that decodes array of "Object" from "data" key
}
struct Object: Codable {
let property1
let property2
let property3
// Code that decodes all properties of Object
}
It doesn't look clean, it requires separate object that simply holds an array, it creates issues with storing to Realm in some cases due to inheritance, it above all produces less readable code.
Generics
My 2nd idea was to try something with Generics, so I made a little something like this:
struct Root<T: Codable>: Codable {
let status: String
let response_code: Int
let messages: [String]?
let data: T?
private enum CodingKeys: String, CodingKey {
case status, response_code, messages, data
}
required public init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self)
status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
messages = try container?.decodeIfPresent([String].self, forKey: .key)
data = try container.decodeIfPresent(T.self, forKey: .data)
}
public func encode(to encoder: Encoder) throws {}
}
With this, I was able to pass both Single Objects and Arrays of Objects to JSONDecoder like this:
let decodedValue = try JSONDecoder().decode(Root<Object>.self, from: data)
// or
let decodedValue = try JSONDecoder().decode(Root<[Object]>.self, from: data)
and this is pretty nice. I can grab the struct I need in .data property of Root struct and use it as I like, as single object or as array of objects. I can store it easily, manipulate however I want without restraints inheritance brings in upper example.
Where this idea fails for my case is when I want to access 'common properties' in some place that isn't sure what T was set to.
This being a simplified explanation of what's actually happening in my app, I will extend it a bit to explain where this Generic solution doesn't work for me and finally ask my question.
Problem and Question
As mentioned at the top, application works with 3 APIs, and all 3 APIs have different Root
structs, and of course, a lot of different "sub-structs" - to name them. I have a single place, single APIResponse
object in application that goes back to the UI part of the application in which I extract 1 readable error from decoded value
, decoded value
being that 'sub-struct', being any of my "specific Objects", Car
, Dog
, House
, Phone
.
With Inheritance solution, I was able to do something like this:
struct APIResponse <T> {
var value: T? {
didSet {
extractErrorDescription()
}
}
var errorDescription: String? = "Oops."
func extractErrorDescription() {
if let responseValue = value as? Root1, let error = responseValue.errors.first {
self.errorDescription = error
}
else if let responseValue = value as? Root2 {
self.errorDescription = responseValue.error
}
else if let responseValue = value as? Root3 {
self.errorDescription = responseValue.message
}
}
}
but with Generics solution, I am unable to do that. If I try to write this same code with Root1
or Root2
or Root3
being constructed as shown in Generics example like this:
func extractErrorDescription() {
if let responseValue = value as? Root1, let error = responseValue.errors.first {
self.errorDescription = error
}
}
I will get error saying Generic parameter 'T' could not be inferred in cast to 'Root1'
and here, where I'm trying to extract the error, I don't know which sub-struct was passed to Root1. Was it Root1<Dog>
or Root1<Phone>
or Root1<Car>
- I don't know how to figure out, and I obviously need to know in order to find out if value is Root1
or Root2
or Root3
.
Solution I am looking for is a solution that would allow me to distinguish between Root
objects with Generics solution shown above, or a solution that allows me to architecture decoding in some completely different way, keeping in mind everything I wrote, especially ability to avoid 'plural' objects
*If JSON doesn't pass JSON validator, please ignore, it was handwritten just for sake of this quesion
**If written Code doesn't run, please ignore, this is more of an architectural question than how to make some piece of code compile.
What you're looking for here is a protocol.
protocol ErrorProviding {
var error: String? { get }
}
I'm intentionally changing errorDescription
to error
because that seems to be what you have in your root types (but you could definitely rename things here).
Then APIResponse requires that:
struct APIResponse<T: ErrorProviding> {
var value: T?
var error: String? { value?.error }
}
And then each root type with special handling implements the protocol:
extension Root1: ErrorProviding {
var error: String? { errors.first }
}
But simple root types that already have the right shape can just declare conformance with no extra implementation required.
extension Root2: ErrorProviding {}
Assuming you want more than just error
, you could make this APIPayload
rather than ErrorProviding
and add any other common requirements.
As a side note, your code will be simpler if you just use Decodable rather than using Codable with empty encode
methods. A type shouldn't conform to Encodable if it can't really be encoded.