Codables: The Basics
It has been a long time we used stuff like NSJSONSerialization
or NSPropertyListSerialization
and dropped the NS
prefix in favor of Swift. It helped converting data from or to JSON / Property List. Creating an instance of your model classes or serialize them to one of the formats would require writing a lot of additional code to convert the values to the correct data types.
Well, the time of these has come to an end and since Swift 4 we got a hand full of new protocols that allow the compiler to generate all the necessary code in the background. But we will still be able to customize the behaviours. More on that in a later chapter. Let us begin with the basics.
The Protocols
There are only two (or three) protocols that you really have to care about: Decodable
and Encodable
. And Codable
, but that is only a type alias for Decodable & Encodable
so it is just a simpler annotation when you want a model do be de- and encodable.
Decodable
This is the protocol when you want to decode data to your model. When you work with external Web-APIs which are JSON, you will most likely use Decodable
to work with you model within your app.
Encodable
For the serialization part, meaning you want to store your model data in a file or send it out to an API, Encodable
is you protocol of choice.
Codable conformant types
Most of the data types you get from the Swift Standard Library are conformant to Decodable
and Encodable
. You can find a complete list in the documentation provided by Apple. I linked the conformant types for Decodable
, but all of these types also work for Encodable.
As you can see, event objects like CGPoint
and CGSize
are listed and completly conform to the Codable
protocol. But they are represented as an array of float values. CGRect
is represented as an array of arrays with float values. This may not be the ideal way of representing the data, but it could be enough if you just want to store data in a local file instead of accessing a self-explaining API.
Date
and Data
are special snowflakes in the Codable space. At least if you want to use a JSONDe-/Encoder. These Coders special settings to modify the strategy on how a Date will be (de-)serialized. ISO-8601 formatted? UNIX timstamp? No problem at all. You can even add your own strategy. More on this in a later chapter.
Example
For convenience I will mostly use JSON in my examples because it is more readable than a full fledged property list.
Lets take this product list as a short example to introduce you to Codables:
let jsonString = """
[{
"title": "Awesome new Book",
"price": 12.99,
"currency": "EUR"
},{
"title": "Fantastic Movie",
"price": 9,
"currency": "USD"
}]
"""
Our goal is to parse this data that can be extended in the future and work with a Product
model within my app.
Model
struct Product: Codable {
let title: String
let price: Double
}
Let us leave out the currency
property at this step. We get back to it in a minute.
Decode JSON
To decode our jsonString
, we have to convert it to a Data
object
let jsonData: Data = Data(jsonString.utf8)
I'm doing it this way, because jsonString.data(using: .utf8)
would result in an optional Data (Data?
) type and I don't want to unwrap it and force unwraps are my most hated lines of code.
As we use JSON in our example, we need a JSONDecoder
that is happily provided by Swift. Most decoders have a simple func decode(_:from:)
function.
let decoder = JSONDecoder()
do {
let deserializedData = try decoder.decode(Array<Product>.self, from: jsonData)
} catch {
print("Decoding Error", error)
}
deserializedData
is now from type Array<Product>
or [Product]
if you like the later one more.
If the decoding fails, you'll get an error explaining what went wrong and where. Like a key that is missing or was the wrong data type.
Recursive De-/Encoding
As I mentioned above, I left the currency value out of the model. Thats because I would like to implement this as an enum
. You can add any other type to your model as they are recursively conformant to the same Codable protocols. This means that as our Product
model is Codable
, our enum has to be Codable
as well.
Enums
For an enum to be En- and/or Decodable, it has to have a raw value if we don't want to write the logics for (de)serializing the object. We will come to that in a later, more advanced guide.
Lets add our currency enum:
struct Product: Codable {
let title: String
let price: Double
let currency: Currency // added the property
}
enum Currency: String, Codable {
case usDollar = "USD"
case euro = "EUR"
}
This is it. We now have a currency
property with an enum. One thing to notice: Any other value (like "GBP"
) will cause the deserialization to fail! And the most sad part is that it will also fail when you set currency to be an optional value.
Encode JSON
Encoding works just like decoding. Just instead of a JSONDecoder
we use a JSONEncoder
:
let encoder = JSONEncoder()
do {
// deserializedData contains our Product array
let serializedProductsData: Data = try encoder.encode(deserializedData)
// Convert Data to String
let serializedProductsString: String = String(data: serializedProductsData, encoding: .utf8)
} catch {
print("Encoding Error", error)
}
At this stage we could also convert the data to a plist by using a PropertyListEncoder
.
Please note, that you'll lose all properties on the encoding side that hadn't been parsed on the decoding side. If you use the example without the currency
property, the encoding will drop that, too.
Wrap it up
I hope you got a little insight in how to work with Codables without using any Frameworks or fancy code. We will get into it and dive a little deeper in the next guides around this topic and how to handle stuff like the enum issue above and how to work with annoying APIs that you can streamline a little bit with Codables.
I will explain how to write custom (de)serialization for your models and how to solve some issues that can occur.