Pablo Garcia

Apps for iPhone, iPad, Apple TV and Apple Watch

Parsing JSON files - Codable, A new hope

Apr 21, 2019

For a long time, I've been using swiftyjson for parsing JSON files. It's a great library and if you, at some point, have had to work with JSON files, I'm pretty sure you've used it, am I right?

But Apple in 2017 gave us an easy way to work with JSON files: Codable. Codable is a typealias for Decodable and Encodable protocols.

Simple data

Let's see it in action. This will be our JSON file, a music album information:



{
 "name": "Love Over Gold",
 "label": "Warner Bros",
 "releaseDate": "1982-09-20T19:00:00Z"
}

And this will be our representation in Swift of the JSON structure



struct Album: Codable {
    let name: String
    let label: String
    let releaseDate: Date
}

Album struct adopt the Codable protocol for encoding and decodign it.



import Foundation

let json = """
{
 "name": "Love Over Gold",
 "label": "Warner Bros",
 "releaseDate": "1982-09-20T19:00:00Z"
}
""".data(using: .utf8)!


struct Album: Codable {
    let name: String
    let label: String
    let releaseDate: Date
}

//Select our decoder and set the date decoding strategy to iso8601(https://en.wikipedia.org/wiki/ISO_8601)
//The decoder will do the conversion between the JSON and our Swift structure
let decoder = JSONDecoder()

decoder.dateDecodingStrategy = .iso8601

let album = try decoder.decode(Album.self, from: json)

print(album)

And the result is....



Album(name: "Love Over Gold", label: "Warner Bros", releaseDate: 1982-09-20 19:00:00 +0000)

What have just happened here?

The Codable protocol, which is actually not one protocol, but two, Decodable and Encodable



public typealias Codable = Decodable & Encodable

public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

public protocol Decodable {
    public init(from decoder: Decoder) throws
}

The Decodable protocol say that we have to implement the init function, but we didn't. So why this example has worked? because the Swift compiler has provided one for us, isn't it cute?

Complex data

Let's complicate a little bit our JSON file.



{
"band": "Dire Straits",
"album": {
    "name": "Love Over Gold",
    "label": "Warner Bros",
    "releaseDate": "1982-09-20T19:00:00Z"
    },
"url": "https://www.markknopfler.com"
}

We have added the name of the band and the url. Decoding is pretty easy:

import Foundation

let json = """
{
"band": "Dire Straits",
"album": {
    "name": "Love Over Gold",
    "label": "Warner Bros",
    "releaseDate": "1982-09-20T19:00:00Z"
    },
"band_url": "https://www.markknopfler.com"
}
""".data(using: .utf8)!


struct Album: Codable {
    let name: String
    let label: String
    let releaseDate: Date
}
struct Band: Codable {
    let band: String
    let album: Album
    let band_url: URL
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let band = try decoder.decode(Band.self, from: json)

print(band)

Result:



Band(band: "Dire Straits", album: __lldb_expr_29.Album(name: "Love Over Gold", label: "Warner Bros", releaseDate: 1982-09-20 19:00:00 +0000), url: https://www.markknopfler.com)

Properties customization

There's one more thing that the compiler generated for us, and that is a private enum called CodingKeys. This enum adopt the CodingKey protocol and is used by the decoder for encoding/decoding, if a property is not defined in this enum, will be ignored.



private enum CodingKeys: String, CodingKey {
  case band
  case album
  case band_url
}

The band_url property doesn't match the Swift naming convention, so let's do something about it. We will state that bandUrl property match with band_url.



struct Band: Codable {
    let band: String
    let album: Album
    let bandUrl: URL

    private enum CodingKeys: String, CodingKey {
        case band
        case album
        case bandUrl = "band_url"
    }
}

Decodable customization

We have customized our properties, now, we're going to customize the decodable by implementing init from decoder.

import Foundation

let json = """
{
"band": "Dire Straits",
"album": {
    "name": "Love Over Gold",
    "label": "Warner Bros",
    "releaseDate": "1982-09-20T21:00:00Z"
    },
"band_url": "https://www.markknopfler.com"
}
""".data(using: .utf8)!
struct Album: Codable {
    let name: String
    let label: String
    let releaseDate: Date
}
struct Band: Codable {
    let band: String
    let album: Album
    let bandUrl: URL

    private enum CodingKeys: String, CodingKey {
        case band
        case album
        case bandUrl = "band_url"
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        band = try container.decode(String.self, forKey: .band)
        album = try container.decode(Album.self, forKey: .album)
        bandUrl = try container.decode(URL.self, forKey: .bandUrl)
    }
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let band = try decoder.decode(Band.self, from: json)

print(band)



Band(band: "Dire Straits", album: __lldb_expr_19.Album(name: "Love Over Gold", label: "Warner Bros", releaseDate: 1982-09-20 21:00:00 +0000), bandUrl: https://www.markknopfler.com)

Conclusion

I think this is a pretty step forward and we should give Codable a try.