• 10 min reading time

20 Aug 2019

How to Create a Generic Swift JSON Parser

A valuable use case for parsing an array of objects in JSON format


Generics were quite confusing for me to start with. I decided to make an end to that, and come up with a valuable use case.


Image by cameradud


As developers, we work with JSON data structure almost daily. Why wouldn’t we make our lives a bit easier while parsing JSON data using generics? That would allow us to write one function and parse any custom list of objects from a remote JSON.


The official Generics definition in Swift is:


Generic code enables you to write flexible, reusable functions and types that can work with any type.


The idea with our JSON parser is to avoid code duplication. Instead of writing a JSON parser for every type of object we want to download, we can write only one function to do that for us.


The aim is to be able to give this function any custom object; be that people, instruments, wizards, or fairies with any given properties, and receive an array of those objects back.


It’d be even better if we could write a generic function that can download and decode a simple JSON array of any given objects. Wouldn’t that be über cool?


It turns out you can do precisely that using Swift generics, in only 30 lines of code. So let’s get to it!


Step One: Class Setup

First, let’s create a class called JSONParser with result typealias that can handle a generic type.


With declaring a typealias result<T>, we set up a generic return type which we use within the Result<[T]:error> value. In this case, our result will either be an array of a generic type objects [T] or an Error.

class JSONParser {
    
    typealias result<T> = (Result<[T], Error>) -> Void
    
    ...
    
}

It’s important to note that including <T> at the end of the typealias name we set it up to be able to handle generic types. T will later be replaced with our own custom object. Further down, we will use the same <T> at the end of a function name that can handle generic types.


Step Two: Function Setup

The next step is to write a function that takes in an object of any type T, URL and give us back the previously defined result<T> as a completion handler.

func downloadList<T: Decodable>(of type: T.Type, 
                                from url: URL, 
                                completion: @escaping result<T>) {
  ...
}

So lets have a look what's happening here.


Further down, you can see that within the function name downloadList<T> we give it the ability to handle generic input type. As we’re later planning to decode our generic type into an array of given objects of type T using JSONDecoder, our type T needs to conform to the Decodable protocol, so we update the function name to downloadList<T: Decodable>.


Step Three: Error Handling

An additional but highly recommended step: in order to be able to understand any errors that our function encounters, we also create a helper enum. So let’s declare an enum called DownloadError and populate it with cases of some common errors.


The code that we’ve written so far:

enum DataError: Error {
    case invalidResponse
    case invalidData
    case decodingError
    case serverError
}

class JSONParser {
    typealias result<T> = (Result<[T], Error>) -> Void
    func downloadList<T: Decodable>(of type: T.Type, 
                                    from url: URL, 
                                    completion: @escaping result<T>) { ... }
}

Step Four: Logic

Next, we have to write the body of the function. The logic of the function should be to parse data from the given URL and decode it into an array of given objects. Having completed all the above, we are now fully equipped to do so.

func downloadList<T: Decodable>(of type: T.Type, 
                                from url: URL, 
                                completion: @escaping result<T>) {
        
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
            }
            
            guard let response = response as? HTTPURLResponse else {
                completion(.failure(DataError.invalidResponse))
                return
            }
            
            if 200 ... 299 ~= response.statusCode {
                if let data = data {
                    do {
                        let decodedData: [T] = try JSONDecoder().decode([T].self, from: data)
                        completion(.success(decodedData))
                    }
                    catch {
                        completion(.failure(DataError.decodingError))
                    }
                } else {
                    completion(.failure(DataError.invalidData))
                }
            } else {
                completion(.failure(DataError.serverError))
            }
        }.resume()
    }

Lets have a look what's happening here.


Firstly, we created an URLSession based on our URL and defined completion block parameters (data, response, error).


Then, we checked if there had been an error that we might not have been aware of (i.e., no internet connection) and write completion with that error if there is one.


If there has been no error, we check if the response we got back is a valid HTTPURLResponse; otherwise, we write completion with .invalidResponse error.


If our response has been valid, we check if the response has a status code somewhere between 200 and 299, which is generally a successful response from a remote API.


If the statusCode is within the given range, we safely unwrap the data and create a JSONDecoder to decode this data into an array of our given generic-type objects. We wrap the decoding logic with the do-catch block, to make sure we intercept a decoding error should there be one.


And that’s all. We’ve just written a JSON parser using generics! What better way to do it than by making a use example and testing it out?


Test Example

To test our shiny new JSON parser, I created a JSON API returning an array of animals — obviously, because we all love animals: Animals JSON.


Then, I created a struct called Animal with coding keys matching the keys within our JSON, and made sure it conforms to the Decodable protocol.


Now, let’s create an instance of our class, create a URL property (make sure to unwrap it safely when using it within an app), and call our downloadList method and print out some results.

struct Animal: Decodable {
    let name: String
    let description: String
    let maxWeightInLbs: Int
    
    private enum CodingKeys: String, CodingKey {
        case name
        case description
        case maxWeightInLbs = "max_weight_lbs"
    }
}

let url = URL(string: "http://www.json-generator.com/api/json/get/cgtNBfTPiq?indent=2")!
let jsonParser = JSONParser()

jsonParser.downloadList(of: Animal.self, from: url) { (result) in
    switch result {
        
    case .failure(let error):
        if error is DataError {
            print(error)
        } else {
            print(error.localizedDescription)
        }
        print(error.localizedDescription)
        
    case .success(let animals):
        print(animals)
        
    }
}

That’s it; we just made a generic JSON parser.


Find this code all in one place.


I hope you enjoyed this article and learned something new on the way.


If you have any thoughts on this article, I would love to hear them!
You can always get in touch with me via Twitter or LinkedIn 🙂


Cheers,


Tim