What is Codable in SwiftUI?

Dec 29, 2020 7 min read
What is Codable in SwiftUI?

The Codable protocol is Apple’s contribution to a standardized approach to encoding and decoding data. That is especially true for the JSON data we get from REST APIs.

The Codable protocol is Apple’s contribution to a standardized approach to encoding and decoding data. That is especially true for the JSON data we get from REST APIs.

The Codable protocols allow for simple JSON decoding that can sometimes take only a couple of lines of code. But they also allow for more sophisticated techniques when you have special needs.

Encoding and decoding JSON

Let’s start with the basics: converting some JSON into Swift structs.

let json = """
[
    {
        "name": "Arturo",
        "age": 30
    },
    {
        "name": "Ramses",
        "age": 31
    }
]
"""

let data = Data(json.utf8)

The last line converts it to a Data object because that’s what Codable decoders work with.

Next we need to define a Swift struct that will hold our finished data:

struct User: Codable {
    var name: String
    var age: Int
}

Now we can go ahead and perform the decode:

let decoder = JSONDecoder()

do {
    let decoded = try decoder.decode([User].self, from: data)
    print(decoded[0].name)
} catch {
    print("Failed to decode JSON")
}

That will print “Arturo”, which is the name of the first user in the JSON.

Converting case

A common problem with JSON is that it will use different formatting for its key names than we want to use in Swift. For example, you might get “first_name” in your JSON and need to convert that to a firstName property.

Now, one obvious solution here is just to change either the JSON or your Swift types so they use the same naming convention, but we’re not going to do that here. Instead, I’m going to assume you have code like this:

let json = """
[
    {
        "first_name": "Arturo",
        "last_name": "Fernandez"
    },
    {
        "first_name": "Andrew",
        "last_name": "Carnegie"
    }
]
"""

let data = Data(json.utf8)

struct User: Codable {
    var firstName: String
    var lastName: String
}

To make this work we need to change only one property in our JSON decoder:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

That instructs Swift to map snake case names (names_written_like_this) to camel case names (namesWrittenLikeThis).

Mapping different key names

If you have JSON keys that are completely different from your Swift properties, you can map them using a CodingKeys enum.

Take a look at this JSON:

let json = """
[
    {
        "user_first_name": "Taylor",
        "user_last_name": "Swift"
        "user_age": 26
    }
]
"""

Those key names aren’t great, and really we’d like to convert that data into a struct like this:

struct User: Codable {
    var firstName: String
    var lastName: String
    var age: Int
}

To make that happen we need to declare a CodingKeys enum: a mapping that Codable can use to convert JSON names into properties for our struct. This is a regular enum that uses strings for its raw values so that we can specify both our property name (the enum case) and the JSON name (the enum value) at the same time. It also needs to conform to the CodingKey protocol, which is what makes this work with the Codable protocol.

So, add this enum to the struct:

enum CodingKeys: String, CodingKey {
    case firstName = "user_first_name"
    case lastName = "user_last_name"
    case age
}

That will now able to decode the JSON as planned.

Note: The enum is called CodingKeys and the protocol is called CodingKey.

Working with ISO-8601 dates

There are many ways of working with dates on the internet, but ISO-8601 is the most common. It encodes the full date in YYYY-MM-DD format, then the letter “T” to signal the start of time information, then the time in HH:MM:SS format, and finally a timezone. The timezone “Z”, short for “Zulu time” is commonly used to mean UTC.

Codable is able to handle ISO-8601 with a built-in date converter. So, given this JSON:

let json = """
[
    {
        "first_name": "Theo",
        "time_of_birth": "1999-04-03T17:30:31Z"
    }
]
"""

We can decode it like this:

struct Baby: Codable {
    var firstName: String
    var timeOfBirth: Date
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

That enables ISO-8601 date parsing, which converts from 1999-04-03T17:30:31Z to a Date instance, while also handling the snake case to camel case conversion.

Working with other common dates

Swift comes with built-in support for three other important date formats. You use them just like you use ISO-8601 dates as shown above, so I’ll just talk about them briefly:

  • The .deferredToDate format is Apple’s own date format, and it tracks the number of seconds and milliseconds since January 1st 2001. This isn’t really useful outside of Apple’s platforms.
  • The .millisecondsSince1970 format tracks the number of seconds and milliseconds since January 1st 1970. This is quite common online.
  • The .secondsSince1970 format tracks the number of whole seconds since January 1st 1970. This extremely common online, and is second only to ISO-8601.

Working with custom dates

If your date format doesn’t match one of the built-in options, don’t despair: Codable can parse custom dates based on a date formatter you create.

let json = """
[
    {
        "first_name": "Jess",
        "graduation_day": "31-08-2001"
    }
]
"""

That uses the date format DD-MM-YYYY, which isn’t one of Swift’s built-in options. Fortunately, you can provide a pre-configured DateFormatter instance as a date decoding strategy, like this:

let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .formatted(formatter)

Working with weird dates

Sometimes you’ll get dates so strange that even DateFormatter can’t handle them. For example, you might get JSON that stores dates using the number of days that have elapsed since January 1st 1970:

let json = """
[
    {
        "first_name": "Jess",
        "graduation_day": 10650
    }
]
"""

To make that work we need to write a custom decoder for the date. Everything else will still be handled by Codable – we’re just providing a custom closure that will process the dates part.

You could try doing some hacky mathematics here such as multiplying the day count by 86400 (the number of seconds in a day), then using the addTimeInterval() method of Date. However, that won’t take into account daylight savings time and other date problems, so a better solution is to use DateComponents and Calendar like this:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom { decoder in
    // pull out the number of days from Codable
    let container = try decoder.singleValueContainer()
    let numberOfDays = try container.decode(Int.self)

    // create a start date of Jan 1st 1970, then a DateComponents instance for our JSON days
    let startDate = Date(timeIntervalSince1970: 0)
    var components = DateComponents()
    components.day = numberOfDays

    // create a Calendar and use it to measure the difference between the two
    let calendar = Calendar(identifier: .gregorian)
    return calendar.date(byAdding: components, to: startDate) ?? Date()
}

Warning: If you have to parse lots of dates, remember that this closure will be run for every single one – make it fast!

Parsing hierarchical data the easy way

Any non-trivial JSON is likely to have hierarchical data – one collection of data nested inside another. For example:

let json = """
[
    {
        "name": {
            "first_name": "Taylor",
            "last_name": "Swift"
        },
        "age": 26
    }
]
"""

Codable is able to handle this just fine, as long as you can describe the relationships clearly.

I find the easiest way to do this is using nested structs, like this:

struct User: Codable {
    struct Name: Codable {
        var firstName: String
        var lastName: String
    }

    var name: Name
    var age: Int
}

The downside is that if you want to read a user’s first name, you need to use user.name.firstName, but at least the actual parsing work is trivial – our existing code works already!

Parsing hierarchical data the hard way

If you want to parse hierarchical data into a flat struct – i.e., you want to be able to write user.firstName rather than user.name.firstName - then you need to do some parsing yourself. This isn’t too hard, though, and Codable makes it beautifully type safe.

First, create the struct you want to end up with:

struct User: Codable {
    var firstName: String
    var lastName: String
    var age: Int
}

Second, we need to define coding keys that describe where data can be found in the hierarchy.

Let’s look at the JSON again:

let json = """
[
    {
        "name": {
            "first_name": "Taylor",
            "last_name": "Swift"
        },
        "age": 26
    }
]
"""

As you can see, at the root there’s a key called “name” and another called “age”, so we need to add that as our root coding keys. Put this inside your struct:

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

Inside “name” were two more keys, “first_name” and “last_name”, so we’re going to create some coding keys for those two. Add this:

enum NameCodingKeys: String, CodingKey {
    case firstName, lastName
}

Now for the hard part: we need to write a custom initializer and custom encode method for our type. Start by adding this empty method to your struct:

init(from decoder: Decoder) throws {
}

Inside there, the first thing we need to do is attempt to pull out a container we can read using the keys of our CodingKeys enum, like this:

let container = try decoder.container(keyedBy: CodingKeys.self)

Once that’s done we can attempt to read our age property. This is done in a type-safe way: you tell it the type you want to decode (Int.self for our age), along with a key name from the CodingKeys enum:

age = try container.decode(Int.self, forKey: .age)

Next we need to dig down one level to read our name data. As you saw earlier, “name” is a top-level key inside our CodingKeys enum, but it’s actually a nested container of other values that we need to read inside. So, we need to pull out that container:

let name = try container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)

And finally we can read two strings for the .firstName and .lastName keys:

firstName = try name.decode(String.self, forKey: .firstName)
lastName = try name.decode(String.self, forKey: .lastName)

That finishes the custom initializer, but we still have one more method to write: encode(to:). This is effectively the reverse of the initializer we just wrote, because it’s job is to convert our properties back into nested container as appropriate.

This means creating a container based on our CodingKeys enum and writing age there, then creating a nested container based on our NameCodingKeys enum, and writing both firstName and lastName there:

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

    var name = container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
    try name.encode(firstName, forKey: .firstName)
    try name.encode(lastName, forKey: .lastName)
}

That completes all our code. With that in place, you can now read the firstName property directly, which is much nicer!

Reference: hacking with swift

Great! Next, complete checkout for full access to ArturoFM.
Welcome back! You've successfully signed in.
You've successfully subscribed to ArturoFM.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.