Quantcast
Channel: Yudiz Solutions Ltd.
Viewing all articles
Browse latest Browse all 595

Encoding and Decoding Custom Types with Swift 4

$
0
0

Overview

Swift 4 comes with the new ideas for encoding and decoding custom types using the new protocol named Codable. Yes, one word for two Protocols Encodable and Decodable.

Using codable we can generate and parse JSON or any other external representations. Codable protocol use to perform both encoding and decoding from and to – JSON by declaring conformance to Codable. Codable have mainly two protocols Encodable and Decodable, which are used to encode and decode our data. This process is known as making our types codable.

Let’s take an example of Company JSON as follows:

{
  "id": "1",
   "name": "Yudiz Solutions Pvt. Ltd",
   "contactNo": “0123456789”,
   "address": "Ahmedabad"
}

So our Swift structure for above JSON should look like this:

class CompanyModel: Codable {

   var id: String
   var name: String
   var address: String
   var contactNo: String

   init(id: String, name: String, address: String, contactNo: String) {
       self.id = id
       self.name = name
       self.address = address
       self.contactNo = contactNo
  }
}

As discussed above, now we would know that Codable is union type including Encodable and Decodable and satisfied all requirements. So now we don’t have to worry about the bidirectional process of conversion by choosing suitable protocol as per requirements. Swift 4 makes conversion very simple by this feature.

Codable comes with a default implementation, and by using this we get useful default behaviour.

Now, Create one object of above class and encode as follow:

let company = CompanyModel(id: "1", name: "Yudiz Solutions Pvt. Ltd.", address: "Ahmedabad, Gujarat", contactNo: "+917965130111")

       // Encoding
       let jsonEncoder = JSONEncoder()
       if let encodedData = try? jsonEncoder.encode(company) {
           if let json = String(data: encodedData, encoding: .utf8) {
               print("Encoded Json: \(json)")
           }

           // Decoding
           let decoder = JSONDecoder()
           if let decodedData = try? decoder.decode(CompanyModel.self, from: encodedData) {
               print("Decoded Data")
               print("Name - \(decodedData.name)")
               print("Address - \(decodedData.address)")
               print("contactNo - \(decodedData.contactNo)")
           }
       }

Output should be look like this:

Encoded Json:

{"id":"1","contactNo":"+917965130111","name":"Yudiz Solutions Pvt. Ltd.","address":"Ahmedabad, Gujarat"}

Decoded Data:
Name – Yudiz Solutions Pvt. Ltd.
Address – Ahmedabad, Gujarat
contactNo – +917965130111

Done!!
For decoding JSON and encoding JSON, this code works great if key names and types are same in both, because there is no need for customization. But what about if keys and types are not same??

Now we take a scenario in which key names and types are different. For that, we will do Customization and Customise key names using CodingKeys protocol.

Customise Key Names using CodingKey protocol:

If the API’s uses different naming keys, and that style not matched with our swift naming guideline, provide alternate keys by adding a nested enumeration named CodingKeys that conforms to the CodingKey protocol, which is used to connect a property to a value in the encoded format.This lets you to name your data structures according to the Swift API Design Guidelines rather than having to match the names, punctuation, and capitalization of the serialisation format that you are modelling.

class CompanyModel : Codable {
     // ...
    enum CodingKeys: String, CodingKey {
       case id
       case name = "company_name"
       case address = "company_address"
       case contactNo = "phone_number"
   }
}

Now our json output as follow:
Encoded Json:

{"company_name":"Yudiz Solutions Pvt. Ltd.","id":"1","phone_number":"+917965130111","company_address":"Ahmedabad, Gujarat"}

We can also format output of JSON by customising output formatting of the JSONEncoder to make it a better human readable form using outputFormatting property. The default value of this property is .compact, we can change it as per our requirement.

jsonEncoder.outputFormatting = .prettyPrinted

Now output looks like this:
Encoded JSON:

{
 "company_name" : "Yudiz Solutions Pvt. Ltd.",
 "id" : "1",
 "phone_number" : "+917965130111",
 "company_address" : "Ahmedabad, Gujarat"
}

Custom encoding and decoding:

If the class structure of your swift file different from its encoded form, we can do custom encoding and decoding by providing a custom implementation of the Encodable and Decodable protocol. In other words, If you want more control over encoding and decoding, you can customise by using inbuilt methods of the Encodable and Decodable protocol.

Let’s take one example of JSON which we want to encode and decode from:

{
   "id": "1",
   "name": "Yudiz Solutions Pvt. Ltd",
   "contactNo": "0123456789",
   "address": {
       "street": "Corporate Road",
       "area": "Prahladnagar",
       "city": "Ahmedabad",
       "zipcode": "380015"
   }
}

For this we can create a simple class which extends Codabale protocol as follow:

class CompanyModel: Codable {

   var id: String
   var name: String
   var contactNo: String
   var street: String
   var area: String
   var city: String
   var zipcode: String

   // Coding Keys
   enum CodingKeys: String, CodingKey {
       case id
       case name = "company_name"
       case address
       case contactNo = "phone_number"
   }

   enum AddressKeys: String, CodingKey {
       case street
       case area
       case city
       case zipcode
   }
}

For a custom implementation of encoding, we implement encode(to encoder:) and for decoding init(from decoder:) methods of Codable protocol to have more control over how the JSON maps to our basic types.

For custom encoding, we need to take the encoder, get a container and encode our JSON values to it. A container has some types as per requirement of our JSON. Following are the types of container:
Keyed Container – It provides the actual values for keys which comes in JSON.
Unkeyed Container – Unkeyed Container gives you ordered values without keys. In the JSONEncoder, this means an array.
Single Value Container – this outputs the raw value without any kind of containing the element.
In order to encode any of our properties, we’ll first need to get a container. Looking at the JSON structure we started with at the top of this post, it’s clear we need a keyed container.

Follow how we encode our JSON into container:

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

       var addressInfo = container.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
       try addressInfo.encode(street, forKey: .street)
       try addressInfo.encode(area, forKey: .area)
       try addressInfo.encode(city, forKey: .city)
       try addressInfo.encode(zipcode, forKey: .zipcode)
   }

Now we need to decode encoded values for that we use the decoder. We can decode all basic types of swift but we need to specify the type while decoding any object otherwise it throws DecodingError.TypeMismatch error and we can handle it.

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

       let addressInfo = try allValues.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
       street = try addressInfo.decode(String.self, forKey: .street)
       area = try addressInfo.decode(String.self, forKey: .area)
       city = try addressInfo.decode(String.self, forKey: .city)
       zipcode = try addressInfo.decode(String.self, forKey: .zipcode)
   }

We can use these encoding and decoding as follow:

let jsonString = """
       {
           "id": "1",
           "company_name": "Yudiz Solutions Pvt. Ltd",
           "phone_number": "0123456789",
           "address": {
               "street": "Corporate Road",
               "area": "Prahladnagar",
               "city": "Ahmedabad",
               "zipcode": "380015"
           }
       }
       """

if let jsonData = jsonString.data(using: .utf8) {
           do {
               let decodedData = try JSONDecoder().decode(CompanyModel.self, from: jsonData)
               print(decodedData.name)
               print(decodedData.contactNo)
               print(decodedData.street)
               print(decodedData.area)
               print(decodedData.city)
               print(decodedData.zipcode)


              let jsonEncoder = JSONEncoder()
               jsonEncoder.outputFormatting = .prettyPrinted
               let encodedJson = try jsonEncoder.encode(decodedData)
               if let json = String(data: encodedJson, encoding: .utf8) {
                   print("Encoded json: \(json)")
               }
           } catch {
               print(error.localizedDescription)
           }
       }

Since the JSON parsing depends on the names of its properties, so you cannot rename them otherwise you have to change the JSON also. You can test that by changing any property and you can see that through breaking JSON encoding/decoding.

That’s all for now! But in future posts, we’ll be looking at Codable again, including handling exceptions generated by encoding/decoding, Dynamic coding keys, handling dates and float values.

For more references:
API Collection Encoding, Decoding, and Serialization
WWDC 2017 – Session 212


Viewing all articles
Browse latest Browse all 595

Trending Articles