Tim Roesner's Blog

Swift: Decode JSON with Codable

Codable is a protocol that has been introduced with Swift 4 and makes encoding and decoding data models to and from JSON significantly easier. In this reference I will discuss how to use it to decode different types of JSON, and what steps you can take if your data model and JSON don’t perfectly line up.

JSON to Data Model

First let’s look at decoding some JSON that matches our data model almost exactly:

[
	{
		"title": "Gone Girl",
		"release_year": 2014,
		"dir": "David Fincher"
	},
	{
		"title": "The Social Network",
		"release_year": 2010,
		"dir": "David Fincher"
	}
]
struct Movie: Codable {
	let title: String
	let releaseYear: Int
	let director: String
	
	enum CodingKeys: String, CodingKey {
		case title
		case releaseYear
		case director = "dir"
	}
}
func decodeMovies(from jsonData: Data) -> [Movie] {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    do {
        return try decoder.decode([Movie].self, from: jsonData)
    } catch {
        print(error.localizedDescription)
        return []
    }
}

With the JSON given above we encounter two issues. First the use of snake case within the JSON and camel case within our own Swift struct. In order to solve this, we tell our JSON decoder to use the .convertFromSnakeCase decoding strategy.
Second the JSON uses an abbreviation for the director key ("dir"). In order to solve this issue we add an enum CodingKeys to our data model, where we can define the string key used within the JSON, and our decoder will use this to automatically map between these keys.
Furthermore we tell the decoder that we expect an array of movies, this works great if we have a flat data source.

Nested JSON

With a nested JSON we often have to create helper structs that we can use to get to our data source, like so:

{
	"response": {
		"status": 200,
		"date": 1593905049
	},
	"data": [
		{
			"title": "The Martian",
			"author": "Andy Weir",
			"release_year": 2011
		},
		{
			"title": "The Circle",
			"author": "Dave Eggers",
			"release_year": 2013
		}
	]
}
struct Book: Codable {
    let title: String
    let author: String
    let releaseYear: Int
}
private struct Root: Codable {
    let data: [Book]
}

func decodeBooks(from jsonData: Data) -> [Book] {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    do {
        let root = try decoder.decode(Root.self, from: jsonData)
        return root.data
    } catch {
        print(error.localizedDescription)
        return []
    }
}

In this example our data is nested within the JSON. Additionally we get some response data from the backend that we do not need for our model. We can safely ignore that and create a helper struct that only unwraps the books information from the nested data object. I declared the Root struct as private since it is only needed where ever we decode the JSON. Last but not least, we have a helper function that is able to return us the nested Book array given some JSON data, with the aid of the decodable Root struct.
The above strategies are most likely enough to decode 90% of the JSON you work with, however sometimes we work with data types that do not have Codable support out of the box. For these cases we have to write our own decode functions.

Custom Decoding

Next let’s look at cases where we are dealing with data types that need a custom decoder implementation. We will also be using the strategies explained above:

{
	"response": {
		"status": 200,
		"date": 1593905053
	},
	"stores": [
		{
			"name": "Apple Union Square",
			"website": "https://www.apple.com/retail/unionsquare/",
			"zip_code": 94108,
			"location": {
				"latitude": 37.7887,
				"longitude": -122.4072
			}
		},
		{
			"name": "Apple Fifth Avenue",
			"website": "https://www.apple.com/retail/fifthavenue/",
			"zip_code": 10153,
			"location": {
				"latitude": 40.7636,
				"longitude": -73.9727
			}
		}
	]
}
import CoreLocation

struct AppleStore {
    let name: String
    let website: URL?
    let zipCode: Int
    let location: CLLocation
}
private struct Root: Decodable {
    let appleStores: [AppleStore]
    
    enum CodingKeys: CodingKey {
        case stores
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let stores = try container.decode([StoreData].self, forKey: .stores)
        appleStores = stores.map { store in
            AppleStore(name: store.name, website: URL(string: store.website), zipCode: store.zipCode, 
            		   location: CLLocation(latitude: store.location.latitude, longitude: store.location.longitude))
        }
    }
}

private struct StoreData: Codable {
    let name: String
    let website: String
    let zipCode: Int
    let location: LocationData
}

private struct LocationData: Codable {
    let latitude: Double
    let longitude: Double
}

func decodeAppleStores(from jsonData: Data) -> [AppleStore] {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    do {
        let root = try decoder.decode(Root.self, from: jsonData)
        return root.appleStores
    } catch {
        print(error.localizedDescription)
        return []
    }
}

In this example we are using CLLoaction within our data model, which does not have Codable conformance out of the box. This means we need to provide a custom decoding strategy. Furthermore the location is nested within our JSON, but stored as a single property within our data model. These mismatches can be handle through implementing some helper struct, which I’m once again declaring private as they are only used to help with decoding.
The main difference to the strategies above is the implementation of init(from decoder: Decoder) throws which is the method that is called by the JSONDecoder. Within it we first create a container based on the CodingKeys that we provided. As with all other Decodable implementations this does not need to be complete, but can be a subset of the keys we’re actually using. Then we decode an array of our helper struct StoreData, which allows us to access the data with the types as it is present within the JSON. Finally we map over that array to generate our array of type AppleStore.
These helper types and implementation of the init(from decoder: Decoder) function make it very pleasant to decode the JSON data at the call site.

Decoding Dates

In addition to a key decoding strategy JSONDecoder also allows us to set a custom DateDecodingStrategy. The following strategies are provided:

// Uses the default decoding strategy provided by the `Date` type
decoder.dateDecodingStrategy = .deferredToDate

decoder.dateDecodingStrategy = .iso8601  // "2020-07-10T02:08:32+00:00"

decoder.dateDecodingStrategy = .secondsSince1970  // 1593905134
decoder.dateDecodingStrategy = .millisecondsSince1970 // 1593905134175

decoder.dateDecodingStrategy = .formatted(DateFormatter)
decoder.dateDecodingStrategy = .custom((Decoder) -> Date)

Summary

As you can see from the examples above, the Decodable protocol is very powerful and versatile when it comes to handling JSON. I hope this reference helped to better understand how to work with our own data types and make decoding a bliss. Additionally there are many online tools that generate the necessary Swift structs based on a JSON input provided by you.

Next Post:

next-post

Building Widgets for iOS 14