Codable in Swift 4.0

Can it replace JSON encode/decode lib out there?

Swift iOS Codable JSON

After I watched WWDC 2017 and heard about Codable, I’m thinking of replacing my current JSON encoder/decoder in my projects or at least use it in a new one.

I’m happy to see Apple finally come up with this encoder/decoder built into Swift standard library since its such a mandatory task nowadays, and for me, I haven’t seen a clear winner in this area. I try to avoid using a third party library as much as possible, so I’m really excited to explore its possibility and limitation.

What am I looking for in encoder/decoder? #

Swift optional type supported #

The most important thing I need is a Swift optional type supported. This is very crucial for me; without this, it is a deal-breaker.

Luckily Codable supports optional type. If you have the following User Object.

struct User: Codable {
var firstName: String
var lastName: String
var middleName: String?
}

These JSON strings are valid.

{
"firstName": "John",
"lastName": "Doe",
"middleName": null
}

{
"firstName": "John",
"lastName": "Doe"
}

Be able to rename properties #

If you have ever work with REST API, you will see that most JSON keys don't use CamelCase naming, but snake_case. Codable also supports rename property keys, and it's very easy to do so.

All you need to do is adding a nested enumeration named CodingKeys that conforms to the CodingKey protocol.

From the above example, we can rename it like this.

struct User: Codable {
var firstName: String
var lastName: String
var middleName: String?

enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
case middleName = "middle_name"
}
}

And you will be able to decode this JSON

{
"first_name": "John",
"last_name": "Doe",
"middle_name": null
}

{
"first_name": "John",
"last_name": "Doe"
}

Custom mapping between JSON and Swift structure. #

There are cases where we have no control over how JSON looks like, be able to have different Swift structure than JSON is a nice-to-have feature.

I will examine two common cases.

  1. Flatten out JSON nested property.
  2. Make a nested structure from flat JSON.

Flatten out JSON #

Let say you have User JSON that contains nested billingAddress property.

{
"name": "John",
"billingAddress": {
"district": "District",
"subDistrict": "Sub District",
"country": "Country",
"postalCode": "Postal Code"
}
}

But somehow you want to layout Swift User like this.

struct User: Codable {
var name: String
var district: String
var subDistrict: String
var country: String
var postalCode: String
}

You need to define two enumerations that each list the complete set of coding keys used on a particular level.

struct User: Codable {
....
enum CodingKeys: String, CodingKey {
case name
case billingAddress
}

enum BillingAddressKeys: String, CodingKey {
case district
case subDistrict
case country
case postalCode
}
}

Since this isn’t direct mapping we need to implementing Decodable's required initializer, init(from:):

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)

let billingAddress = try values.nestedContainer(keyedBy: BillingAddressKeys.self, forKey: .billingAddress)
district = try billingAddress.decode(String.self, forKey: .district)
subDistrict = try billingAddress.decode(String.self, forKey: .subDistrict)
country = try billingAddress.decode(String.self, forKey: .country)
postalCode = try billingAddress.decode(String.self, forKey: .postalCode)
}

And same apply to Encodable protocol, a custom encode(to:):

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)

var billingAddress = container.nestedContainer(keyedBy: BillingAddressKeys.self, forKey: .billingAddress)
try billingAddress.encode(district, forKey: .district)
try billingAddress.encode(subDistrict, forKey: .subDistrict)
try billingAddress.encode(country, forKey: .country)
try billingAddress.encode(postalCode, forKey: .postalCode)
}

Nested structure #

This is the opposite of what we just did; we got JSON like this:

{
"name": "John",
"district": "District",
"subDistrict": "Sub District",
"country": "Country",
"postalCode": "Postal Code"
}

And want this Swift structure:

struct User: Codable {
var name: String
var billingAddress: BillingAddress
}

struct BillingAddress: Codable {
var district: String
var subDistrict: String
var country: String
var postalCode: String
}

Following are what we need to implement:

struct User: Codable {
....
enum CodingKeys: String, CodingKey {
case name
case billingAddress
case district
case subDistrict
case country
case postalCode
}
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
billingAddress = try BillingAddress(from: decoder)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(billingAddress.district, forKey: .district)
try container.encode(billingAddress.subDistrict, forKey: .subDistrict)
try container.encode(billingAddress.country, forKey: .country)
try container.encode(billingAddress.postalCode, forKey: .postalCode)
}

Conclusion #

Codable pass all my criteria; it can do what I needed with easy to understand syntax. The only aspect that I didn't touch is performance (I think it would be good). My conclusion is I definitely use it for my next project.


Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Tweet Share

If you enjoy this article, you can subscribe to the weekly newsletter.

Every Friday, you’ll get a quick recap of all articles and tips posted on this site — entirely for free.

← Home