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 encode/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 lib, since its such a mandatory tasks nowdays and for me I haven’t seen a clear winner in this area. I try to avoid using third part library as much as possible, so I’m really excited to explore its possibility and limitation.

What I looking for in encode/decode?

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 support this. If you have 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 doesn’t use CamelCase naming, but snake_case. Codable also support rename property keys and its very easy to do so.

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

From 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 look like, be able to have different Swift structure than JSON is nice-to-have feature.

I will test on 2 common cases.

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

Flatten out JSON

Let say you have User JSON that contain 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 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.


← Home