Close modal

Blog Post

Swift Decodable for easy and elegant JSON

Development
Tue 17 April 2018
0 Comments


You'd be hard pressed to find a mobile developer who has not had to consume a feed in JSON, and probably had to encode it also. Historically you would likely have found that the decoding part can get messy without way of a well defined schema that can do the heavy lifting for you (like JSON.net in C#), and have to create a murky set of parsing constructors or factories. Fear no more, Swift 4 introduces codable and decodable which finally ease much of this burden and create code that is less encumbered with the mechanics of deserialisation.

Simple case

Let's say you want to decode something from a JSON string, taking a simple object on its own, here is a playground that will do that:

import Foundation

struct User: Decodable {
    let id: String
    let name: String
    let country: String
}

let jsonString = "{\"id\": \"happyfeet12\", \"name\": \"Happy Feet\", \"country\": \"Antarctica\"}"
let userObject = try JSONDecoder().decode(User.self, from: jsonString.data(using: .utf8)!)
print(userObject)

Unsurprisingly the result on the console is: User(id: "happyfeet12", name: "Happy Feet", country: "Antarctica")

What's more profound is that all the heavy lifting of specifying key names and types has been done for us! Simply confirming to Decodable allows the swift language to make inferences about the mapping are usually correct. Just like your class members and variables swift will assume camel case for the field names in the JSON and map accordingly, using the type of that member from the class or struct (we use struct generally as it's simplier to construct)

Snake (or some other) case

For many people we can stop here, but still in many other cases you might find the backend service or data source doesn't use camel case for the field names, it may use 'snake' case, which would call userId as user_id instead, you have probably seen this.

Let's take another simple example that builds on what we did previously:

struct User: Decodable {
    let id: String
    let name: String
    let country: String
}

struct Post: Decodable {
    let userId: String
    let imageUrl: String
}

struct PostFeed: Decodable {
    let users: [User]
    let posts: [Post]
}

let jsonString = "{\"users\":[{\"id\":\"jn23\",\"name\":\"John\",\"country\":\"Canada\"},{\"id\":\"feebz\",\"name\":\"Phoebe\",\"country\":\"Wales\"},{\"id\":\"maz00\",\"name\":\"Mark\",\"country\":\"New Zealand\"}],\"posts\":[{\"user_id\":\"maz00\",\"image_url\":\"https://placekitten.com/g/720/480\"},{\"user_id\":\"jn23\",\"image_url\":\"https://placekitten.com/g/480/480\"}]}"

do {
    let feedObject = try JSONDecoder().decode(PostFeed.self, from: jsonString.data(using: .utf8)!)
    print(feedObject)
} catch let jsonError {
    print(jsonError)
}

However, you get to integration and discover that your JSON looks as follows:

{
  "users": [
    {
      "id": "jn23",
      "name": "John",
      "country": "Canada"
    },
    {
      "id": "feebz",
      "name": "Phoebe",
      "country": "Wales"
    },
    {
      "id": "maz00",
      "name": "Mark",
      "country": "New Zealand"
    }
  ],
  "posts": [
    {
      "user_id": "maz00",
      "image_url": "https://placekitten.com/g/720/480"
    },
    {
      "user_id": "jn23",
      "image_url": "https://placekitten.com/g/480/480"
    }
  ]
}

Looks good at the start right? Then you read down to see user_id and image_url in the Post blob, you might even try it and see, but unfortunately you will get a Swift.DecodingError error, such as:

"No value associated with key CodingKeys(stringValue: \"userId\", intValue: nil) (\"userId\").", underlyingError: nil))

So what do we do now? Since the backend isn't likely in our control, or can even be changed withour affecting other systems, we'll have to take a different approach.

Add the enum CodingKeys to the Post struct, just like below:

struct Post: Decodable {
    let userId: String
    let imageUrl: String

    enum CodingKeys : String, CodingKey {
        case userId = "user_id"
        case imageUrl = "image_url"
    }
}

Now, trying the playground again. It works, and we can resume creating whatever awesome app we set out to do.

Custom decoding (or missing members)

Now you know how to override the coding keys, but what if the default logic for decoding members doesn't meet your requirements. Did you know that a missing value will cause another Swift.DecodingError.

Let's imagine we get the following responses from the API for a good outcome:

{
    "success": true,
    "result": "#CS45636"
}

If there's something wrong with the request or outcome, we would get the following response:

{
    "success": false,
    "error": "Your request could not be submitted at this time."
}

Seems pretty straight forward right? Let's struct this up:

struct APIResult: Decodable {
    let success: Bool
    let result: String?
    let error: String?
}

Except, if a key is missing (even if it's marked optional as a type, there will be an error thrown).

What's needed is to override the init(from decoder: Decoder) and don't foget throws as DecodingError can be created here. Here's take two:

struct APIResult {
    let success: Bool
    let result: String?
    let error: String?
}


extension APIResult : Decodable {
    enum CustomKeys: String, CodingKey { // declaring our keys
        case success = "success"
        case result = "result"
        case error = "error"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CustomKeys.self) // defining our (keyed) container

        // extract keys manually
        let success: Bool = try container.decode(Bool.self, forKey: .success)

        // Here is the reason for a custom init, as it may not be present
        let result = try? container.decode(String.self, forKey: .result)
        let error = try? container.decode(String.self, forKey: .error)

        self.init(success: success, result: result, error: error) // initializing our struct
    }
}

As you can see, we also added the CustomKeys enum as per the last example, in this case not so much because we need to override the camelCase names, but simply because we couldn't refer to it implicitly otherwise and the decoder requires the name of the field to decode as provided (CustomKeys only exists because we created it, right?).

Let's run an example for both of these cases:

let successString = "{ \"success\": true, \"result\": \"#CS45636\" }"
let successResponse = try JSONDecoder().decode(APIResult.self, from: successString.data(using: .utf8)!)
print(successResponse)

let errorString = "{ \"success\": false, \"error\": \"Your request could not be submitted at this time.\" }"
let errorResponse = try JSONDecoder().decode(APIResult.self, from: errorString.data(using: .utf8)!)
print(errorResponse)

You should see: APIResult(success: true, result: Optional("#CS45636"), error: nil) APIResult(success: false, result: nil, error: Optional("Your request could not be submitted at this time."))

That wasn't so painful, was it? Generally, it's much less work than the old custom frameworks or mechanisms we did before Swift4, even when we have to overide things.

Conclusion

As you might have noticed, I focused exclusively on Decodable and not Codable.

The reasons are two fold:

  • Generally, it is much more common to decode and consume JSON than it is to encode it.
  • More importantly, encoding is easier, assuming your types conform to decoable because types do not have to be inferred and cannot be missing as the type and presence of values are bound to the concrete class.

However, it's just as easy, a quick snippet of the user object from above:

let encoder = JSONEncoder()
let userJSON = try encoder.encode(userObject)

That's right, you don't even have to specify the class because generics/RTTI can do it for you. Enjoy your newfound encoding powers!