JSON Deocding in Swift 4

You’d be forgiven for thinking iOS 11 is only about augmented reality, so for anyone yet to have a use for AR in their apps, perhaps there’s not a ton of work this summer? But there is one change for iOS 11 that I’ve been looking forward to adding to my apps - JSON decoding thats built in to Swift 4. To date I’ve been making use of the great AlamoFire and SwiftyJSON libraries to process this for me. But being able to replace any 3rd party code for your own is always preferable for reasons of maintenance, portability, security, etc etc etc. Plus, JSONDecoder has reduced the lines of code I’m using, and has added safety.

Here I’m loading JSON from a remote service, and JSONDecoder is returning this to me as a Swift tuple. Its also possible to encode JSON using almost the same process in reverse, with JSONEncoder, but I’m not going to touch on that here.

Defining a Model

The first step is to define the model for the JSON you’re receiving, this is done in the form of a struct that conforms to Codable. Lets imagine we want to parse the following JSON:

1
2
3
4
{"name":"Wellard",
"colour":"Black",
"age":4,
"breed_name":"Belgian Tervuren"}

The struct we’d write would look something like this

1
2
3
4
5
6
struct Dog: Codable {
var name: String
var colour: String
var age: Int
var breed_name: String
}

Remapping Variable Names

Great, except JSON often has a couple of quirks that doesn’t always map perfectly to swift. JSON regularly uses snake_case to name variables, which doesn’t match nicely with Swift’s camelCase. So we’ll want to rename some. A CodingKeys enum allows us to do this. Somewhat irritatingly, we must include all keys in this, even ones we’re not renaming, so this could become laborious for larger JSON objects. Possibly something Sourcery could help you out with?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Dog: Codable {

enum CodingKeys : String, CodingKey {
case name
case colour
case age
case breedName = "breed_name"
}

var name: String
var colour: String
var age: Int
var breedName: String
}

Notice above we use the JSON name in the enum, and those we want to rename we give a string for the new variable name. Then in the body of the struct, use the new Swift variable name. Its also worth mentioning here, that you can also ignore values. If your JSON returns something you have no need for, just don’t add the variable name to your struct, and Swift will discard it.

Wrappers and Nested Elements

JSON often features wrappers, like returning objects inside a ‘results’ object. These can be processed by nesting it inside another struct. What if we’re returned an array of dogs inside a ‘kennel’ element, the JSON might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
{"kennel":[

{"name":"Wellard",
"colour":"Black",
"age":4,
"breed_name":"Belgian Tervuren"},

{"name":"Bouncer",
"colour":"Golden",
"age":7,
"breed_name":"Labrador Retriever"}

]}

To decode this we need to define a struct that contains an array of the Dog structs we created earlier.

1
2
3
4
5
struct Kennel: Codable {

var kennel: [Dog]

}

The approach is similar if our JSON contains nested elements. Lets say we want to list each dog’s favourite toy, each toy has values like type and colour, then we’d define a third struct like so:

1
2
3
4
5
6
7
struct Toy: Codable {

var name: String
var colour: String
var type: String

}

Then in the Dogs struct we’d add a variable for toy, with the type of Toy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Dog: Codable {

enum CodingKeys : String, CodingKey {
case name
case colour
case age
case breedName = "breed_name"
case favouriteToy = "favourite_toy"
}

var name: String
var colour: String
var age: Int
var breedName: String
var favouriteToy: Toy
}

Decoding

Once we’ve defined our model to match the JSON input, decoding it into Swift is essentially trivial. We use Swift 4’s new JSONDecoder and the decode method. This takes two arguments, firstly the model. As our JSON is wrapped in a Kennel value we use Kennel.self. Then we pass the data we received from our URLSession. Wrap this in a try? to catch if the JSON you’re feeding in doesn’t match your model.

1
2
3
4
if let decoded = try? JSONDecoder().decode(Kennel.self,
from: data!) {
print(decoded)
}

Done.

To bring this all together, here’s the code we’ve created so far. Lets assume the JSON data we’re provided is…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{"kennel":[

{"name":"Wellard",
"colour":"Black",
"age":4,
"breed_name":"Belgian Tervuren",
"favourite_toy":{

"name":"Squeaky",
"colour":"Yellow",
"type":"Ball"}

},

{"name":"Bouncer",
"colour":"Golden",
"age":7,
"breed_name":"Labrador Retriever",
"favourite_toy":{

"name":"Mr Heffalump",
"colour":"Blue",
"type":"Stuffed Elephant"}

}]}

Then our Swift code will look like…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
struct Dog: Codable {

enum CodingKeys : String, CodingKey {
case name
case colour
case age
case breedName = "breed_name"
case favouriteToy = "favourite_toy"
}

var name: String
var colour: String
var age: Int
var breedName: String
var favouriteToy: Toy
}

struct Toy: Codable {

var name: String
var colour: String
var type: String

}

struct Kennel: Codable {

var kennel: [Dog]

}

let sessionConfig = URLSessionConfiguration.default
let urlSession = URLSession(configuration: sessionConfig)
let url = URL.init(string: "http://supercooldogjsondatabase.com")

let dataTask = urlSession.dataTask(with: url!) { data, response, error in

if error != nil {
print(error?.localizedDescription ?? "")
} else {
if let data = data {
if let decoded = try? JSONDecoder().decode(Kennel.self,
from: data) {
print(decoded)
}
}
}
}

dataTask.resume()

Where Next

Try this out on some actual JSON to get used to using it, MAAS is one of the JSON APIs I use in Mars Watch to get weather reports from Mars. Its a pretty simple set of data to use to try out JSONDecoder, and one that I used all the techniques above to get the best results for me.