Swift 4 Decodable: Beyond The Basics 📦
One of the features that I was looking forward to this year WWDC was Codable
, which is just a type alias of the Encodable
and Decodable
protocols.
I’ve just spent a whole week shifting my Swift projects from using custom JSON parsers to Decodable
(while removing a lot of code! 🎉), this post showcases what I’ve learned along the way.
Basics
If you haven’t seen it already, I suggest you to watch the related WWDC session (the
Codable
part starts past 23 minutes).
In short: you can now convert a set of data from a JSON Object or Property List to an equivalent Struct
or Class
, basically without writing a single line of code.
Here’s an example:
import Foundation
struct Swifter: Decodable {
let fullName: String
let id: Int
let twitter: URL
}
let json = """
{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
""".data(using: .utf8)! // our data in native (JSON) format
let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // Decoding our data
print(myStruct) // decoded!!!!!
What the Compiler Does Without Telling Us
If we look at the Decodable
documentation, we see that the protocol requires the implementation of a init(from: Decoder)
method.
We didn’t implemented it in the Playground: why did it work?
It’s for the same reason why we don’t have to implement a Swifter
initializer, but we can still go ahead and initialize our struct: the Swift compiler provides one for us! 🙌
Conforming to Decodable
All of the above is great and just works™ as long as all we need to parse is a subsets of primitives (strings, numbers, bools, etc) or other structures that conform to the Decodable
protocol.
But what about parsing more “complex structures”? Well, in this case we have to do some work.
Implementing init(from: Decoder)
⚠️ This part might be a bit trickier to understand: everything will be clear with the examples below!
Before diving into our own implementation of this initializer, let’s take a look at the main players:
The Decoder
As the name implies, the Decoder
transforms something into something else: in our case this means moving from a native format (e.g. JSON) into an in-memory representation.
We will focus on two of the Decoder’s functions:
container<Key>(keyedBy: Key.Type)
singleValueContainer()
In both cases, the Decoder
returns a (Data) Container.
With the first function, the Decoder
returns a keyed container, KeyedDecodingContainer:
to reach the actual data, we must first tell the container which keys to look for (more on this later!).
The second function tells the decoder that there’s no key: the returned container, SingleValueDecodingContainer
, is actually the data that we want!
The Containers
Thanks to our Decoder
we’ve moved from a raw native format to a structure that we can play with (our containers). Time to extract our data! Let’s take a look at the two containers that we’ve just discovered:
KeyedDecodingContainer
In this case we know that our container is keyed, you can think of this container as a dictionary [Key: Any]
.
Different keys can hold different types of data: which is why the container offers several decode(Type:forKey:)
methods.
This method is where the magic happens: by calling it, the container returns us our data’s value of the given type for the given key (examples below!).
Most importantly, the container offers the generic method decode<T>(T.Type, forKey: K) throws -> T where T: Decodable
which means that any type, as long as it conforms to Decodable
, can be used with this function! 🎉🎉
SingleValueDecodingContainer
Everything works as above, just without any keys.
Implementing our init(from: Decoder)
We’ve seen all the players that will help us go from data stored in our disk to data that we can use in our App: let’s put them all together!
Take the playground at the start of the article for example: instead of letting the compiler doing it for us, let’s implement our own init(from: Decoder)
.
Step 1: Choosing The Right Decoder
The example’s data is a JSON object, therefore we will use the Swift Library’s JSONDecoder
.
let decoder = JSONDecoder()
⚠️ JSON and P-list encoders and decoders are embedded in the Swift Library: you can write your own coders to support different formats!
Step 2: Determining The Right Container
In our case the data is keyed:
{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
To reach "Federico Zanetello"
we must ask for the value of key "fullName"
, to reach 123456
we must ask for the valued of index "id"
, etc.
Therefore, we must use a KeyedDecodingContainer
Container (by calling the Decoder’s method container<Key>(keyedBy: Key.Type)
).
But before doing so, as requested by the method, we must declare our keys: Key
is actually a protocol and the easiest way to implement it is by declaring our keys as an enum
of type String
:
enum MyStructKeys: String, CodingKey {
case fullName = "fullName"
case id = "id"
case twitter = "twitter"
}
Note: you don’t have to write = “…” in each case: but for clarity’s sake I’ve chosen to write it.
Now that we have our keys set up, we can go on and create our container:
let container = try decoder.container(keyedBy: MyStructKeys.self)
Step 3: Extracting Our Data
Finally, we must convert the container’s data into something that we can use in our app:
let fullName: String = try container.decode(String.self, forKey: .fullName)
let id: Int = try container.decode(Int.self, forKey: .id)
let twitter: URL = try container.decode(URL.self, forKey: .twitter)
Step 4: Initializing our Struct/Class
We can use the default Swifter initializer:
let myStruct = Swifter(fullName: fullName, id: id, twitter: twitter)
Voila! We’ve just implemented Decodable all by ourselves! 👏🏻👏🏻 Here’s the final playground:
//: Playground - noun: a place where people can play
import Foundation
struct Swifter {
let fullName: String
let id: Int
let twitter: URL
init(fullName: String, id: Int, twitter: URL) { // default struct initializer
self.fullName = fullName
self.id = id
self.twitter = twitter
}
}
extension Swifter: Decodable {
enum MyStructKeys: String, CodingKey { // declaring our keys
case fullName = "fullName"
case id = "id"
case twitter = "twitter"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MyStructKeys.self) // defining our (keyed) container
let fullName: String = try container.decode(String.self, forKey: .fullName) // extracting the data
let id: Int = try container.decode(Int.self, forKey: .id) // extracting the data
let twitter: URL = try container.decode(URL.self, forKey: .twitter) // extracting the data
self.init(fullName: fullName, id: id, twitter: twitter) // initializing our struct
}
}
let json = """
{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
""".data(using: .utf8)! // our native (JSON) data
let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // decoding our data
print(myStruct) // decoded!
Going further (More Playgrounds!)
Now that our Swifter
struct conforms to Decodable
, any other struct/class/etc that contains such data can automatically decode Swifter for free. For example:
Arrays
import Foundation
struct Swifter: Decodable {
let fullName: String
let id: Int
let twitter: URL
}
let json = """
[{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
},{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
},{
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}]
""".data(using: .utf8)! // our data in native format
let myStructArray = try JSONDecoder().decode([Swifter].self, from: json)
myStructArray.forEach { print($0) } // decoded!!!!!
Dictionaries
import Foundation
struct Swifter: Codable {
let fullName: String
let id: Int
let twitter: URL
}
let json = """
{
"one": {
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
},
"two": {
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
},
"three": {
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
}
""".data(using: .utf8)! // our data in native format
let myStructDictionary = try JSONDecoder().decode([String: Swifter].self, from: json)
myStructDictionary.forEach { print("\($0.key): \($0.value)") } // decoded!!!!!
Enums
import Foundation
struct Swifter: Decodable {
let fullName: String
let id: Int
let twitter: URL
}
enum SwifterOrBool: Decodable {
case swifter(Swifter)
case bool(Bool)
}
extension SwifterOrBool: Decodable {
enum CodingKeys: String, CodingKey {
case swifter, bool
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let swifter = try container.decodeIfPresent(Swifter.self, forKey: .swifter) {
self = .swifter(swifter)
} else {
self = .bool(try container.decode(Bool.self, forKey: .bool))
}
}
}
let json = """
[{
"swifter": {
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
},
{ "bool": true },
{ "bool": false },
{
"swifter": {
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
}
}]
""".data(using: .utf8)! // our native (JSON) data
let myEnumArray = try JSONDecoder().decode([SwifterOrBool].self, from: json) // decoding our data
myEnumArray.forEach { print($0) } // decoded!
More Complex Structs
import Foundation
struct Swifter: Decodable {
let fullName: String
let id: Int
let twitter: URL
}
struct MoreComplexStruct: Decodable {
let swifter: Swifter
let lovesSwift: Bool
}
let json = """
{
"swifter": {
"fullName": "Federico Zanetello",
"id": 123456,
"twitter": "http://twitter.com/zntfdr"
},
"lovesSwift": true
}
""".data(using: .utf8)! // our data in native format
let myMoreComplexStruct = try JSONDecoder().decode(MoreComplexStruct.self, from: json)
print(myMoreComplexStruct.swifter) // decoded!!!!!
And so on!
Before Departing
In all probability, during your first Decodable
implementations, something will go wrong: maybe it’s a key mismatch, maybe it’s a type mismatch, etc.
To detect all of these errors early, I suggest you to use Swift Playgrounds with error handling as much as possible:
do {
let myStruct = try JSONDecoder().decode(Swifter.self, from: json) // do your decoding here
} catch {
print(error) // any decoding error will be printed here!
}
You can go even deeper by splitting different types of Decoding Errors.
Even on the first Xcode 9 beta, the error messages are clear and on point 💯.
Conclusions
I was really looking forward to this new Swift 4 feature and I’m very happy with its implementation. Time to drop all those custom JSON parsers!
That’s all for today! Happy Decoding!