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.

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