Swift 4’s Codable
One last battle for serialization
It is almost certain that at some point in your app development journey, you needed to (or you will soon :]) serialize some object or value and parse JSON response to your model. If I am right, then this might worth your time.
In Swift, we used to use NSCoding
protocol to serialize our objects. To conform to NSCoding
we very often ended up making our classes inherit from NSObject
and then after some cha cha and samba and some freakishly long amount of time, we finally manage to implement the expected init(coder:)
and encode(with:)
methods. But what about structs
right? Well, most people found the solution in introducing third party frameworks to their projects. Hopefully, the fight for serialization and mapping has come to an end.
What now?
Swift 4 introduces a protocol called Codable
which is a composition of two other protocols: Encodable
& Decodable
. Codable
allows us to serialize and deserialize classes
, structs
and enums
with almost no effort. Most types including String
, Int
, Array
, Dictionary
are conforming to this protocol. So when we create a new type containing properties which already conform to Codable
, we do not need to write one more line of code. The Person
type below conforms to Codable
just like that:
struct Person: Codable {
let name: String
let age: Int
let friends: [Person]?
}
Serializing Person
to JSON:
let person = Person(name: "Brad", age: 53, friends: nil)
// Sorry Bradlet encoder = JSONEncoder()
let jsonData = try encoder.encode(person)String(data: jsonData, encoding: .utf8)! // {"name":"Brad","age":53}
Voilà, person value converted to data just like that.
Now let’s deserialize:
let decoder = JSONDecoder()let decoded = try decoder.decode(Person.self, from: jsonData)
type(of: decoded) // Person
What’s in it for me?
There two awesome things that I found Codable
useful for:
- Mapping
- Archival & Unarchival
Mapping
Say you receive your responses as a JSON string (even better if it comes as encoded data). It would be delicious to initialize our types with that JSON string right? Let’s write it down:
protocol Mappable: Decodable {
init?(jsonString: String)
}extension Mappable {
init?(jsonString: String) {
guard let data = jsonString.data(using: .utf8) else {
return nil
}
self = try! JSONDecoder().decode(Self.self, from: data)
// I used force unwrap for simplicity.
// It is better to deal with exception using do-catch.
}
}
Thanks to powerful protocol extensions, once again, we need not to do anything further with our types (multiline strings with triple quotation marks below, are also introduced in Swift 4). And we do not to conform to Codable
which includes Encodable
also since we only want to map from JSON
to Mappable
types.
let jsonString = """
{
\"name\": \"Brad\",
\"age\": 53
}
"""
A little bit of refactoring to Person
:
struct Person: Mappable {
let name: String
let age: Int
let friends: [Person]?
}let newPerson = Person(jsonString: jsonString)
type(of: newPerson) // Person?
Adding optionality to Mappable
initializer is needed to avoid possible failures due to missing fields in response. And when we make a property optional (friends
in this case), Codable
will still work if the optional field is not included in the data.
If you were using some third party framework like ObjectMapper
for this, great news, you no longer need to. Not only it already offers the same functionality for mapping, it also saves you from finding the values with the keys by subscript. It infers the keys from the property name. Swift 4 also provides the CodingKey
protocol to grant you the freedom of naming your properties as you like:
struct Person: Mappable {
let alias: String
let age: Int
let comrades: [Person]?
private enum CodingKeys : String, CodingKey {
case alias = "name"
case age
case comrades = "friends"
}
}
I have changed name
to alias
and friends
to comrades
because I can.
Transformations
Apart from modifying the naming of response parameters with CodingKey
, we can also make structural changes with the response. One case would be using your own Coordinate
type instead of latitude
and longitude
values received from the service call. Or I could wish to store the date string in various formats of Date
type. To achieve that, we are going to need to implement the init(from:)
method of Decodable
protocol ourselves.
Let’s reimplement the Person
type:
struct Person: Mappable {
let birth: Date
let position: Coordinate
let name: String? enum CodingKeys: String, CodingKey {
case birth = "birth_date"
// Response will contain lat & long but
// we will store as coordinate
case lat
case long
case name
} init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self) let birthString = try values.decode(String.self, forKey: .birth)
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy"
birth = formatter.date(from: birthString)! // Parse lat and long to initialize coordinate
let lat = try values.decode(Float.self, forKey: .lat)
let long = try values.decode(Float.self, forKey: .long)
position = Coordinate(lat: lat, long: long) name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
And the usage:
let jsonString = """
{
\"birth_date\": \"11/21/1982\",
\"lat\": 29.321,
\"long\": 36.119
}
"""
let person = Person(jsonString: jsonString)
person!.position.lat // 29.321
type(of: person!.birth) // Date.Type
person!.name // nil
We used CodingKeys
enum to be able to parse lat and long from the response and a DateFormatter
to parse birth property as Date
instead of String
(I will come back to this in a moment). Notice how the name
is nil since the jsonString
does not contain name
key and value. For our optional types, we have to use decodeIfPresent
method instead of decode
method. The down side is, if we were to perform transformation only on one property of a type with say ten properties, we will have to implement the assignments for all others in the usual manner too since the default extension cannot be used anymore.
The better way to transform Date
would be to update our Mappable
extension like the following:
...
let jsonDecoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy"
jsonDecoder.dateDecodingStrategy = .formatted(formatter)self = try! jsonDecoder.decode(Self.self, from: data)
...
This way, instead of creating a Date
type from the String
by ourselves, we tell the JSONDecoder how to parse String
to Date
. So this:
let birthString = try values.decode(String.self, forKey: .birth)
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy"
birth = formatter.date(from: birthString)!
Is now just one line for every Date
assignment:
birth = try values.decode(Date.self, forKey: .birth)
And if the only transformation we need is about Date
type, we no longer need to implement init(from decoder: Decoder)
since JSONDecoder
will be able to handle it with the default implementation.
Archival & Unarchival
NSKeyedArchiver
& NSKeyedUnarchiver
also supports Codable
. Here is how we archive a value:
NSKeyedArchiver.archiveRootObject(person, toFile: "file")
Along with your classes that conform to NSCoding
, now all your types that conform to Codable
can be handled by NSKeyedArchiver
.
Unarchival, here you go:
guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: "file") as? Data else { return }
If you chose to encode your value with JSONEncoder
and then archive it, you can decode it with JSONDecoder
after unarchival.
This feature allows us to persistently store our Codable
types.
Final Notes
While using Codable
, you may face with various cases such as initialization of your Codable
type as an array when received as root level array in response, or how to perform transformations (thanks to Evan Roth and Göksel Köksal for pointing those out in the comments). If you face with such issues or have any improvements in mind please do not hesitate to comment those below :]