March 25, 2020

Parse JSON from file and URL with Swift

Parse JSON from file and URL with Swift

In todays tutorial we are going to learn how to read a JSON file with Swift, but that file could be a local JSON file or a hosted JSON file. I have a GitHub repo setup for this project that contains the local file but also the hosted file.

Before we get started we need to get our data.json file setup. I would suggest copying the content from the file I have already created which you can find here.

Now that you have the file, we can start coding.

Step 1: Create the local JSON file

The first thing that we need to do is create our local JSON file. To add a blank file to our project we can simply select the blank file when creating a new file in Xcode.

The following images show the steps that you need to take to create the json file:

Next we need to select the blank file. To do this scroll all the way to the bottom of the file options:

The last thing we need to do is save the file. Make sure to name the file data.json:

Step 2: Create the Codeable Struct

Now that we have our data.json file we need to create our codeable Struct that will represent the structure of our JSON data.

I have created a struct called DemoData that represents the structure of the JSON data:

struct DemoData: Codable {
    let title: String
    let description: String
}

Step 3: Read the local file

Let's create the function that will read the local data.json file. The function that we will create will take the file name as an argument and it will return Data? as the return type.

The below code will allow us to read the file and return the data of the file:

private func readLocalFile(forName name: String) -> Data? {
    do {
        if let bundlePath = Bundle.main.path(forResource: name,
                                             ofType: "json"),
            let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
            return jsonData
        }
    } catch {
        print(error)
    }
    
    return nil
}

There is nothing too complicated going on here. We will pass a file name as an argument and then use that file name to get a resource that matches the file name in our main bundle.

If this file exists in the main bundle we can use the bundlePath when initializing a new String. This string will get the contents of the file and convert it to .utf8 data. When creating a new String with contentsOfFile the initialization can fail. In order to handle this we wrap everything in a do try catch statement.

If the file exists we will return the data from the file, if it doesn't, either because there was an error or the file doesn't exist, we will return nil.

Step 4: Create the JSON Parse method

The parsing method is super basic. Since Codeable has been added to Swift, it has become incredibly simple to decode JSON. So in our parse method we will be using the JSONDecoder().decode  method to convert our JSON to DemoData.

This method is a bit dirty as we are going to use it for decoding as well as printing out the title and the description.

Add the following method to your code:

private func parse(jsonData: Data) {
    do {
        let decodedData = try JSONDecoder().decode(DemoData.self,
                                                   from: jsonData)
        
        print("Title: ", decodedData.title)
        print("Description: ", decodedData.description)
        print("===================================")
    } catch {
        print("decode error")
    }
}

Step 5: Create loadJson from URL method

We have created the method to read the local file, we have created the method to decode the content of the JSON file and print it to our console, so technically we should be able to run the app. Before we run the app I want us to create the method that will load the JSON file from a URL.

Below is the code for the loadJson function:

private func loadJson(fromURLString urlString: String,
                      completion: @escaping (Result<Data, Error>) -> Void) {
    if let url = URL(string: urlString) {
        let urlSession = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
            }
            
            if let data = data {
                completion(.success(data))
            }
        }
        
        urlSession.resume()
    }
}

The loadJson method takes two arguments, the first being the urlString and the second is completion.

The completion takes an argument of type Result<Data, Error>. Result is a swift 5 feature and is perfect for this use case. If we have an error we can return .failure(error) and if it is successful we can return .success(data) when using the Result type which we will see later on.

The first thing that we do is try and convert the urlString to a URL. This will return an optional URL so we use if let to unwrap the optional URL.

If the URL is not nil we will create a new URLSession with a .default configuration. We will then pass the url to the dataTask method, as well as a closure to handle the response. This response takes 3 arguments, data, response, error.

We then check if there is an error, if there is we call the completion with a .failure(error). If there is no error then we call the completion with .success(data). This makes it easier for us in the completion handler to handle the failed or successful result.

Step 6: Use the two methods we created

Now that we have both methods created we need to call them. Let's start with reading the local data.json file.

To do this I have put the following code in the viewDidLoad function:

if let localData = self.readLocalFile(forName: "data") {
    self.parse(jsonData: localData)
}

It is super simple to use the readLocalFile method. We pass through the file name and if there is data we will pass that data to our parse method. When the parse method runs and there are no errors, the title and description from our data.json file will be printed to the console.

Now that we have read and printed the contents of our local JSON file, let's read and print the contents of the hosted JSON file.

Just below the code we added to read the local file, add the following code:

let urlString = "https://raw.githubusercontent.com/programmingwithswift/ReadJSONFileURL/master/hostedDataFile.json"

self.loadJson(fromURLString: urlString) { (result) in
    switch result {
    case .success(let data):
        self.parse(jsonData: data)
    case .failure(let error):
        print(error)
    }
}

We have create a new variable called urlString, this is just to have things look a bit cleaner when we call loadJson.

Next we call our loadJson method. To do this we pass the urlString through as well as a closure that takes one argument, Result<Data, Error>.

Inside the closure we switch on the result. The two cases that we have are .failure and .success, which are the same as what we had in our loadJson method that we created in Step 5.

If the result is successful, we will pass the data that we get from our loadJson method and pass it to our parse method. If loadJson failed, we will print out the error that it passed to this closure.

Now that all the code is done you can build and run the app. The content from both the local and the hosted files should be printed in your console.

Conclusion

Reading a JSON file that is local or one that requires a url is very easy with Swift 5 and later. Before it would have been a bit of an irritation, but codeable makes a world of difference, and with just a few lines we are able to read a JSON file from anywhere.

If you want to see the full source code, you can find it here.