Skip to content

Instantly share code, notes, and snippets.

@SpencerCurtis
Last active January 3, 2019 18:08
Show Gist options
  • Save SpencerCurtis/e2d4abf38b2b7db0332da1c7b6b181d0 to your computer and use it in GitHub Desktop.
Save SpencerCurtis/e2d4abf38b2b7db0332da1c7b6b181d0 to your computer and use it in GitHub Desktop.

Objective 1 - understand and explain Codable's data types

Codable is actually a combination of the Encodable and Decodable protocols. Encodable turns your model object's values into a different format such as JSON or a Plist, while Decodable does the opposite. Each protocol has just a single method. These methods will get called when you call encode or decode. Before we look at how to implement these methods ourselves, you will need to understand a few data types.

Looking at both Encodable's encode(to encoder: Encoder) throws and Decodable's init(from decoder: Decoder) throws on a surface level, we see that they have an Encoder and a Decoder argument respectively. They are both protocols, which are then adopted by JSONEncoder/Decoder and PropertyListEncoder/Decoder, and any custom encoder and decoder that you may create. Both encoders and decoders have essentially the same properties and methods. We'll look at the Decoder's properties and methods below:

Properties:

  • codingPath: Allows you to see the current path in the decoding. It isn't used in the implementations of Codable's methods, but will show up in any errors thrown by the methods.
  • userInfo: Allows you to pass some information into this dictionary to use when decoding data. This is not often used but can be useful for more advanced implementations.

Methods:

Each of these methods will return a different kind of "container". A container represents a level or node of the JSON, property list, etc.

  • func container<Key>(keyedBy: CodingKey.Protocol) -> KeyedDecodingContainer<Key>: A keyed container essentially represents a group of key-value pairs. This You can think of the container holding values in the same way as a dictionary. You'll also notice that it has an argument that conforms to the CodingKey protocol. That means if you are going to use a keyed container, you must make an enum for your coding keys.
  • func singleValueContainer() -> SingleValueDecodingContainer: A single value container simply represents a single value, such as a string, number, etc.
  • func unkeyedContainer() -> UnkeyedDecodingContainer: An unkeyed container contains a group of values without keys, so this closely mirrors an array.

Using these containers, we are able to navigate through the data and decode it in the same way that it is formatted. Or if we are using Encoder's version of these containers, we can encode our model objects in any format we choose.

Objective 2 - implement encode(to:) and init(from:) methods to customize encoding and decoding

Up to this point you have likely used Codable by simply conforming to the protocol and letting magically it do the work for you. While there is nothing wrong with this, let's look at why you may want to implement Codable's methods yourself.

When working with nested JSON and letting Codable do most of the work for us, we have to format our model objects in a specific way, like having multiple nested structs or classes inside of each other:

struct Building: Codable {
    
    struct Floor: Codable {
        
        struct Room: Codable {
            var numberOfChairs: Int
        }
        
        var rooms: [Room]
    }
    
    var floors: [Floor]
}

While there is nothing wrong with doing this, it isn't commonly done besides when using Codable. When implementing Codable ourselves, have the ability to flatten out the information into a single model object. We'll also look at another reason to implement it ourselves in the next objective as well.

Decoding Example

Let's now look at some simple JSON and how to decode it ourselves. Let's say we have this JSON:

{
  "name": "John Johnson",
  "age": 30
}

Our model object would look something like this to begin with:

struct Person: Codable {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

Before we add the init(from decoder) initializer, we need to look at the JSON and think about which values we want from it. For every value we want, we need to add its key to a coding keys enum. In this case, we'll want the name and age:

enum CodingKeys: String, CodingKey {
    case name
    case age
}

As a refresher, there is nothing special about the enum's name CodingKeys. We could call it PersonCodingKeys or whatever else we wanted to.

Now that we have the coding keys, we can implement init(from decoder):

init(from decoder: Decoder) throws {

    // 1
    let container = try decoder.container(keyedBy: CodingKeys.self)
        
    // 2
    let name = try container.decode(String.self, forKey: .name)
    let age = try container.decode(Int.self, forKey: .age)
        
    // 3
    self.name = name
    self.age = age
}
  • 1: We create a container from the decoder argument.
  • 2: We then call the container's decode method. When using a keyed container, it has type and key arguments. There are many different variations of this method that allow you to choose the type the value should be decoded as, then you choose the key from the coding keys you passed into the container(keyedBy:) method from step 1. This method throws, so remember to call try. There is no need to set up a do-try-catch block since the initializer itself that we are in throws. If we think of the keyed container as a dictionary, then decode is the way that we get values out of it with a key.
  • 3: We now have the name and age decoded, so we can finish the initializer by setting the instance variable's values. Alternatively, you could call the Person's init(name: String, age: Int) to accomplish the same thing.

Let's add another level to the JSON:

{
  "name": "John",
  "age": 30,
  "favoriteNumbers": [
    42,
    10,
    13
  ]
}

Make sure to add a new case to the CodingKeys enum for the "favoriteNumbers" key-value pair. Once that is done, we can add to the the init(from decoder:) from above:

// 1
var favoriteNumbersContainer = try container.nestedUnkeyedContainer(forKey: .favoriteNumbers)

// 2        
var favoriteNumbers: [Int] = []
        
while !favoriteNumbersContainer.isAtEnd {
    let number = try favoriteNumbersContainer.decode(Int.self)
    favoriteNumbers.append(number)
}
  • 1: Again, for each new "level" of JSON, we need to create a new container of some kind. This new level of JSON is an array, so we'll make an unkeyed container. A couple things to note here are that the container must be a variable, which we'll come back to in step 2. The other thing is that we call the nestedUnkeyedContainer method on the container we made previously. This is important to remember. We don't always get containers from the decoder, just the top level of the data. From there, you are able to get "nested" containers from other containers. This allows you to drill down into each level of the data.
  • 2: There are a few ways that you can decode the data from an unkeyed container. The important thing to remember is that every time you call decode in an unkeyed container, that value also gets removed from it, which is why the container needs to be a variable. In the example above we are using the container's isAtEnd property to see if the container is empty or not. Until it is empty, the while loop will keep decoding the Ints in it.

If we try use decode to decode a value that doesn't exist the method will throw an error and the model object will not get initialized at all. Now what if you have some key-value pairs in your JSON that are only there sometimes? Luckily, there is a sibling function called decodeIfPresent that works the same as decode, but when it tries to decode the value it won't throw an error if there isn't one. When using decodeIfPresent the value will be returned as an optional.

Encoding Example

Now that you've seen decoding JSON, encoding is very similar. One thing to take into account is that if you are encoding your JSON back to an API, it is going to expect the JSON to be structured in a specific way. More often than not, it will be structured like it was when you fetched it. So if you are "flattening" out the JSON into a single model object when you're decoding, but need to send that information back to the API, we need to encode it back to its original format (or whatever format is required by the API).

Let's take the Person model object and implement Encodable:

func encode(to encoder: Encoder) throws {
        
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(favoriteNumbers, forKey: .favoriteNumbers)
    }

We begin similarly by creating a keyed encoding container using our CodingKeys from earlier. From there, we just need to tell the encoder to encode each value with a given key. Note that for encoding arrays, you can also use a for-in loop to go through the contents of the array and encode them individually if you want. In this case, the outcome is identical.

Just like Decodable's decodeIfPresent method, there is a parallel encodeIfPresent method for encoding optional values without throwing errors if the value is nil.

At this point, that is about everything you need to know about implementing Encodable and Decodable yourself. Obviously you will see more complicated, nested JSON than this. When you do, you will just make a new container of the correct kind for each level until you are able to access and encode and/or decode the information that you need for your model object.

There is one situation that is slightly more involved in order to decode, however. We'll look at it in the next objective.

Objective 3 - use a custom init(from:) method to decode objects whose properties contain both the key and value from a json dictionary

Sometimes you will come across JSON with irregular keys; keys that do not have a static name such as "name" or "user", etc. Instead the key appears to be some unique identifier. We will run into a problem because each one is different and there is no way to effectively make a coding key for every identifier in order to decode it.

Let's say we have a social networking app with a basic model object representing a user profile that has a username and their bio. The JSON could look like this:

{
  "-LTurQ1JOOrdH6sWNw3b" : {
    "username" : "Bill",
    "bio" : "Insert witty bio here"
  },
  "-LTurQ5STtM66lhpx8hp" : {
    "username" : "Stan",
    "bio" : "Excelsior!"
  },
  "-LTurQ5Mce055e0Two8G" : {
    "username" : "Edgar",
    "bio" : "Quoth the raven"
  }
}

Each of these 3 profiles has another piece of information which is the unique identifier key containing the profile. It is essentially a part of the profile's information, and we will need to know what the user's identifier is in order to access it, for example to update their bio or username. Let's create a model object to represent this information:

struct UserProfile {
    let username: String
    let bio: String
    let identifier: String
}

We need to make a generic set of coding keys that are able to be created when the decoding container is created:

struct GenericCodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init?(stringValue: String) {
        self.stringValue = stringValue
    }
    
    init?(intValue: Int) {
        self.intValue = intValue
        self.stringValue = "\(intValue)"
    }
}

The CodingKey protocol requires a property and initializer for both string String and Int values. What we're doing here is simply setting both properties' value. The best part of this is that we can create a keyed container with this GenericCodingKeys struct and it will automatically make a key for each key-value pair, even if it is a key like the JSON above has that is impossible to know beforehand.

Another thing we need to do is create a parent data type for the UserProfile that we can use to implement init(from decoder:):

struct UserProfiles: Decodable {
    
    enum CodingKeys: String, CodingKey {
        case username
        case bio
    }
  
    var profiles: [UserProfile]
}

We've got a basic struct that has an array of UserProfile objects, which is mirroring the structure of the JSON, along with a CodingKeys enum for the statically keyed key-value pairs.

You may have noticed that the UserProfile struct isn't conforming to Codable. That's because we're going to decode each profile in UserProfiles' init(from decoder:). Let's add that initializer:

struct UserProfiles: Decodable {
    
    enum CodingKeys: String, CodingKey {
        case username
        case bio
    }
    
    init(from decoder: Decoder) throws {
        // 1
        let container = try decoder.container(keyedBy: GenericCodingKeys.self)
        
        var profiles: [UserProfile] = []
        
        // 2
        for key in container.allKeys { // ["-LTurQ5STtM66lhpx8hp", "-LTurQ5Mce055e0Two8G", "-LTurQ1JOOrdH6sWNw3b"]
            
            // 3
            let profileContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: key)
            
            let username = try profileContainer.decode(String.self, forKey: .username)
            let bio = try profileContainer.decode(String.self, forKey: .bio)
            
            // 4
            let profile = UserProfile(username: username, bio: bio, identifier: key.stringValue)
            
            profiles.append(profile)
        }
        
        self.profiles = profiles
    }
    
    var profiles: [UserProfile]
}
  • 1: In the first step, we begin by creating a keyed decoding container, but we are using the GenericCodingKeys struct instead of CodingKeys.
  • 2: Once we have the container, we can get each key by looping through it's allKeys property. Each key in this case would be each unique identifier.
  • 3: We are then able to drill down a level into the JSON by using the key provided by the loop to decode the profile's username and bio using the normal CodingKeys enum.
  • 4: We can now initialize the individual UserProfile with the decoded values and the identifier key.

It can be a bit confusing to see how this all works. You shouldn't run into many situations where this is applicable, but you should keep knowledge of it handy in case you do have to deal with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment