Swift 4 Codable in Real life

      1 Comment on Swift 4 Codable in Real life

I was one of those people screaming to the miracle in the WWDC room when they first presented Codable in swift 4. I could not wait to put my hands on this new swift feature and starting using it.I was one of those people screaming to the miracle in the WWDC room when they first presented Codable in swift 4. I could not wait to put my hands on this new swift feature and starting using it.

Personally I believe that this swift 4 feature is awesome, but when it comes to real life it’s not always applicable. If for example you are trying to map an entity from different sources you might need to create two different objects to fulfil the way the entity is described in each source (song from Spotify, song from iTunes, song from wherever). Another common scenario is that the same object is returned as a json with different keys depending on the api that returns it. While this might indicate a certain level of unhealthiness of the backend you are talking to, it is still a problem I saw multiple times that might affect our choices in our app architecture, expecially if it is one of those problems that you must live with and build on top of it (if it never happened to you you are very lucky).

In this article we will talk about this scenario: Codable for the same object, different keys from different json responses.

So far in swift there was no real direct conversion from json to structured data, but Codable promises to allow a native conversion, and not just from json encoded objects, but potentially from (and to) any encoded format.

Before Codable…

Let’s suppose we have a service that returns students in a school. The json for a single student would be something like

{
    "id": 12345,
    "name": "Giuseppe",
    "last_name": "Lanza",
    "age": 31
}

So let’s create our entity in app.

public struct Student {
   public let id: Int
   public let name: String
   public let last_name: String
   public let age: Int
}

With previous version of swift to decode our json into a Student we had to convert the json data into a dictionary and then parse it manually. The code was resulting something like this:

public struct Student {
   public let id: Int
   public let name: String
   public let last_name: String
   public let age: Int

   public init?(fromDictionary dictionary: [String: Any]) {
       guard let id = dictionary["id"] as? Int,
           let name = dictionary["name"] as? String,
           let last_name = dictionary["last_name"] as? String,
           let age = dictionary["age"] as? Int else {
               return nil
           }
       self.id = id
       self.name = name
       self.last_name = last_name
       self.age = age     
   }
}

This code might change based on your app architecture and on the fact that you might be using some helper library, but it won’t be much different.

If something goes wrong in the decoding, what happens is that your return value is nil with no further informations around what actually went wrong in the decoding.

If you want a better error handling, than your init should become throwable, the decoding should be done in a separate guard statement for each key and each else should throw an error for missing key, unexpected value or type mismatch.

… After Codable

Codable saves you from all of that. You can get for free the initialiser to decode the object, the error explaining what went wrong and potentially you can get serialisation/deserialization from different data sources (other than dictionaries/json). How?

public struct Student: Codable {
   public let id: Int
   public let name: String
   public let last_name: String
   public let age: Int
}

Yes… it is simple as that.

Focusing on the Decodable part of the Codable feature, making your class conforming to Codable  (or just Decodable ) will give you for free a basic initialiser so that in you networking layer, when you receive your jsonData you will be able to do something like that:

let decoder = JSONDecoder() //or any other Decoder
let student = try? decoder.decode(Student.self, from: jsonData)

Pretty cool right? But where this magic is coming from? Did you notice that our variableNames has exactly the same name as the field we are mapping in the json? Well that’s the key.

Decodable explained

The compiler in compile time creates for us an initialiser for the Encodable&Decodable protocols (aka Codable) and uses for keyed archivers the var names as keys and their types to map the object. But life it’s not perfect and in swift programming we usually use camel case for var names so that last_name would be in our apps most likely lastName. But what happens when we change the var name?

public struct Student: Codable {
   public let id: Int
   public let name: String
   public let lastName: String
   public let age: Int
}

let jsonData = ...
let decoder = JSONDecoder()
do {
    let student = try decoder.decode(Student.self, from: jsonData)
} catch {
    print(error)
}

You will get an error of course.

keyNotFound(__lldb_expr_45.Student.(CodingKeys in _8FCE288CA3061383EB8EA2B77A9AC5E6).lastName, Swift.DecodingError.Context(codingPath: [], debugDescription: “No value associated with key lastName (\”lastName\”).”, underlyingError: nil))

A very readable one. Here we understand immediately that there is a key that was not found. Specifically “No value associated with key lastName (\”lastName\”).” so, the default implementation of Decodable (in this case) is trying to map the var lastName with a field that in the json does not exist. Apple gives us a way to deal with this kind of var name mismatch.

public struct Student: Codable {
    public let id: Int
    public let name: String
    public let lastName: String
    public let age: Int
    
    public enum CodingKeys: String, CodingKey {
        case id
        case name
        case lastName = "last_name"
        case age
    }
}

We need to add an enum called CodingKeys  in our struct to define an explicit custom mapping. This CodingKeys  enum is implicitly created by the compiler in compile time, but of course the default key will be the variable name, and that’s the reason why without this specification for a custom key we were having an error in our previous code sample. Running our example this time will give us a valid Student  instance.

Also the initialiser is not voodoo magic. It is implicitly created by the compiler in compile time, but it looks something like this:

public struct Student: Codable {
    public let id: Int
    public let name: String
    public let lastName: String
    public let age: Int
    
    public enum CodingKeys: String, CodingKey {
        case id
        case name
        case lastName = "last_name"
        case age
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        lastName = try container.decode(String.self, forKey: .lastName)
        age = try container.decode(Int.self, forKey: .age)
    }
}

So, first we tell to the decoder which keys will be used to get a key container, then using this key container we can decode each var using the associated key and its value type.

The generated initialiser will always have CodingKeys.self  as CodingKey, and that’s what makes the nested structure with name CodingKeys  so special. Creating a custom initialiser you are of course free to change this fact and use a different CodingKey compliant data structure, but in this case you will have to write all the decode for all of your variables.

Internally each decode(_ forKey:)  method will look like this

func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer.Key) throws -> Bool {
    guard encodedObject.contain(key.stringValue) else {
        throw ... //key not found
    }
    
    guard let decodedValue = encodedObject[key.stringValue] else {
        throw ... //value not found
    }
    
    guard let typedValue = decodedValue as? Bool else {
        throw ... //type mismatch
    }

    return typedValue
}

Obviously the code in this snippet is not the real implementation of the method but it should just give you a general idea about what actually really happens for each one of those calls. In this example I chose the Bool decoder method, but there is a decode method for each decodable Type, including a generic one where T is Decodable  (so you can have a Decodable class containing a var that is itself Decodable).

The initialiser for decoder can be explicitly created if you need some further customisation on object creation, but in general you will not really need to write it explicitly because swift compiler does it for you.

So far I think I wrote here nothing you cannot find elsewhere. You are probably thinking “cool, I already know all this stuff“. That’s right, but this is not the scope of this post. The title is “Codable in Real life“, So what can actually happen in real life?

The problem

What happens if https://myService.com/students?id=12345  returns

{
    "id": 12345,
    "name": "Giuseppe",
    "last_name": "Lanza",
    "age": 31
}

But https://myService.com/class?id=1234 returns

{
    "id": 1234,
    "name": "Physics",
    "students": [
        {
            "id": 12345,
            "name": "Giuseppe",
            "lastName": "Lanza",
            "age": 31
        }
    ]
}

Well that’s trouble, because I would like to have my SchoolClass object to be something like this:

public struct SchoolClass: Codable {
   let id: Int
   let name: String
   let students: [Student]
}

Which is totally legit, because Codable is able to deal with nested Codable types.

The solution

So here what I want is to be able to decode my student for both services, but the two services returns different keys for the lastName (or last_name) field.

Apple doesn’t help us with that. Following the normal Codable way we must create a different Student entity for each one of our non conformant services. Well that sucks…

But we can do something about that.

For instance it’s my personal opinion that the decodable entity should not know the keys used in the backend for the mapping. This should be responsibility of the closest object to the backend which is in our networking layer. If there must be an object responsible of knowing the mapping keys, that must be there.

Can we inject in our decodable object the keys we want to use, based on the service that we are querying?

YES WE CAN!

First let’s take a look at the enum nested in our Student class. It is a String type enum that conforms to a protocol: CodingKey. We can then define a protocol for StudentKeys inheriting from CodingKey.

public protocol StudentKeys: CodingKey {
    static var id: Self { get }
    static var name: Self { get }
    static var lastName: Self { get }
    static var age: Self { get }
}

Why all static, and why do they all are of type Self?

Do you remember the init(from decoder: Decoder)  we explicitly wrote before?

When you define the container you ask to the decoder for a container object using a specific set of keys. The method container.decode(Int.self, forKey: .id) in the parameter “forKey” wants an object of the type <CodingKey> used before to obtain the container.

We protocolized the keys that we are expecting in our Student struct. Now what we have to do is to make injectable the keys in our entity so that each api requester can inject keys according to their own responses.

To do that we must change a little our Student struct:

public struct Student<Keys: StudentKeys>: Codable {
    public let id: Int
    public let name: String
    public let lastName: String
    public let age: Int
    
    public enum CodingKeys: CodingKey {
        case id
        case name
        case lastName
        case age
        
        public var stringValue: String {
            switch self {
            case .id: return Keys.id.stringValue
            case .name: return Keys.name.stringValue
            case .lastName: return Keys.lastName.stringValue
            case .age: return Keys.age.stringValue
            }
        }
        
        public var intValue: Int? { return nil }
    }
}

Our Student  became a generic type that accepts Keys as generic parameter. These keys must be conforming StudentKeys .

Our CodingKeys  also changed. This enum is no longer a String enum type but to remain conformant to CodingKey it must have the variables stringValue  and intValue .

We can write them so that stringValue  maps CodingKeys from the Keys  generic type. The swift compiler will still create the initialiser for us and since the default implementation creates a container using the nested CodingKeys  enum, we are all set to use our Student  injecting our keys.

Also our SchoolClass  has to change a little bit to accept the generic parameter for StudentsKeys .

public struct SchoolClass<StKeys: StudentKeys>: Codable {
    let id: Int
    let name: String
    let students: [Student<StKeys>]
}

How a StudentKeys implementation should look like? For this particular example I chosed a struct.

///This struct defines the keys for the entity Student, when it is 
///returned from GETStudent request
public struct GETStudentKeys: StudentKeys {
    public static var id: GETStudentKeys = GETStudentKeys(stringValue: "id")!
    public static var name: GETStudentKeys = GETStudentKeys(stringValue: "name")!
    public static var lastName: GETStudentKeys = GETStudentKeys(stringValue: "last_name")!
    public static var age: GETStudentKeys = GETStudentKeys(stringValue: "age")!

    public let stringValue: String
    public var intValue: Int? { return nil }
    
    public init?(intValue: Int) { return nil }
    public init?(stringValue: String) { self.stringValue = stringValue }
}

///This struct defines the keys for the entity Student, when it is 
///returned from GETClass request
public struct GETClassStudentKeys: StudentKeys {
    public static var id: GETClassStudentKeys = GETClassStudentKeys(stringValue: "id")!
    public static var name: GETClassStudentKeys = GETClassStudentKeys(stringValue: "name")!
    public static var lastName: GETClassStudentKeys = GETClassStudentKeys(stringValue: "lastName")!
    public static var age: GETClassStudentKeys = GETClassStudentKeys(stringValue: "age")!
    
    public let stringValue: String
    public var intValue: Int? { return nil }
    
    public init?(intValue: Int) { return nil }
    public init?(stringValue: String) { self.stringValue = stringValue }
}

Now we have our two studentKeys. One to decode the https://myService.com/students?id=12345 and the other to decode the https://myService.com/class?id=1234

let studentJsonData = ...
let decoder = JSONDecoder()
do {
    let student = try decoder.decode(Student<GETStudentKeys>.self, from: studentJsonData)
    print(student)
} catch {
    print(error)
}


let schoolClassJsonData = ...
do {
    let student = try decoder.decode(SchoolClass<GETClassStudentKeys>.self, from: schoolClassJsonData)
    print(student)
} catch {
    print(error)
}

 

One thought on “Swift 4 Codable in Real life

  1. Pingback: 2018: Year in Review | agostini.tech

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.