Decoding Typesafe Responses

Last time, we looked at how to define the requests in our network layer in a way that keeps our code clean.

But we only looked at one side of the equation because all we were doing was fetching data from the server. It’s a good start, but we’ll probably need to decode it before we can use it in our app.

Will we be able to continue to use this DRY compositional approach in order to handle responses as well?

(Spoiler alert: Yes)

Today, we’ll have a look at:

  • How to match a request to a response.
  • How to use composition in order to build higher-level objects.
  • How we can optimize our code to make the most common path easy.

Let’s get to it!

Matching requests to response with the Resource type

It’s nice to know we’re able to fetch Data from our server. But now we need to couple these requests with a transformation to Data to the right type. Let’s refresh our memory with a new request that fetches a Person with a given ID:

public struct GetPersonRequest: RequestTemplate {
    public let method: HTTPMethod = .GET
    public var path: String { return "/api/people/\(personId)" }

    private let personId: Int

    public init(personId: Int) {
        self.personId = personId
    }
}

Last time, we took a step back to think about how we could represent a request. Let’s do the same with a response. What does it mean for us to handle a response?

We’re going to receive some Data, and we need to transform it into a type that the application can use. Often, it’s going to be a model that we define, but let’s not limit ourselves to that. Maybe you’ll want to do something different, like save the contents of the response to Core Data so they can be fetched later.

I think the best way to represent this type of transformation is with a closure from Data to the type that we want.

So let’s see this in action. Let’s take the response from GetPersonRequet and parse them into a Person struct. Lucky for us, Apple provides the Codable protocol to make this easy.

struct Person: Codable {
    let name: String
    let birthyear: String
}

Now we need to figure out how exactly do we get data from our request parsed into this struct? I propose creating a Resource type that will encompass both the request with and a closure to transform it into what we need.

Here’s how that would look for fetching People:

struct GetPersonResource {
    let request: GetPersonRequest
    let decodeResponse: (Data) throws -> Person
}

Not too bad, right? Note that we marked the closure with throws, since we’ll be using JsonDecoder and we’ll want to surface its errors. All sorts of errors can happen at this level of our application, so it’s important we make them visible ☝️

Now that we’ve defined a resource for Person, how would this look for Starship?

struct GetStarshipResource {
    let request: GetStarshipRequest
    let decodeResponse: (Data) throws -> Starship
}

Your “Don’t Repeat Yourself” alarm from last time should already be sounding off. Let’s generalize this concept to something we can use with any RequestTemplate.

struct Resource<T> {
    let request: RequestTemplate
    let decodeResponse: (Data) throws -> T
}

Alright. Now we have something we can work with! Instead of creating a struct for every Resource pair we can think of, instead we’ve used a generic type to represent the type we want to decode.

I also want to point out an important design decision here: we’re not coupling the request to a given type of response. We could parse the same request in multiple different ways if we wanted to. I find this to be a cool feature to have available for more complex applications.

Now let’s take this Resource struct for a spin!

Our first resource

Creating a resource should be fairly straightforward. Let’s see what decoding /people/:id would look like using its default initializer:

let request = GetPersonRequest(id: 1)
let getPerson = Resource(request: request) { data -> Person in
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    let people = try decoder.decode(Person.self, from: data)

    return people
}

I can imagine quite a few of our resources (like getting a starship) would look like this, don’t you think? Let’s see if we can define an initializer that would make this creating a bit more straightforward.

extension Resource {
    init(request: RequestTemplate, type: T.Type) {
        self.init(request: request) { data in
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase

            let decoded = try decoder.decode(type, from: data)

            return decoded
        }
    }
}

This makes things much nicer for ourselves. Our code from above can now become:

let request = GetPersonRequest(id: 1)
let getPerson = Resource(request: request, type: Person.self)

Finally, much like we did in the last article with RequestTemplate, we can define a new method on SWAPIto execute Resource .

extension SWAPI {
    func fetchResource<T>(_ resource: Resource<T>,
                          completion: @escaping (Result<T, Error>) -> Void) {
        guard let urlRequest = URLRequest(with: resource.request, baseURL: self.baseURL) else {
            return
        }

        resumeDataTask(withRequest: urlRequest) { result in
            switch result {
            case let .success(data):
                do {
                    let decoded = try resource.decodeResponse(data)
                    completion(.success(decoded))
                } catch {
                    completion(.failure(error))
                }
            case let .failure(error):
                completion(.failure(error))
            }
        }
    }
}

This fetchResource method looks a lot like what we did with

Now when we run the following code, we’re able to decode our response!

let api = SWAPI()
api.fetchResource(getPerson) { result in
    print(result)
}

//success(__lldb_expr_1.Person(name: "Luke Skywalker", birthYear: "19BBY"))

Wrapping up

How many times have you run into libraries that were supposed to make your life easier, but didn’t accommodate your specific use case? Hopefully, we’ve built something here that can do both!

Here’s what I consider the key things to remember:

  • We used small building blocks (RequestTemplate) to create bigger blocks (Resources). This gives us confidence in the abstraction that we built.
  • Creating a new resource is simple and succinct. How to create a new resource is clear and concise.
  • We’ve made the common case easy, while remaining flexible enough for cases that would require custom parsing.

Next time, we’ll look into how to parse paged resources (like /people and /starships) and how we can easily integrate them into a table view.