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.