Parsing and working with Codable in Swift 4



    JSON format has become very popular, it is usually used for transferring data and performing requests in client-server applications. JSON parsing requires encoding / decoding tools of this format, and Apple has recently updated them. In this article, we will look at the JSON parsing methods using the Decodable protocol , compare the Codable protocol with the predecessor NSCoding , evaluate the advantages and disadvantages, analyze all of the specific examples, and consider some of the features encountered in the implementation of the protocols.


    What is Codable?

    At WWDC2017, along with the new version of Swift 4, Apple introduced new data encoding / decoding tools that are implemented by the following three protocols:

    - Codable
    - Encodable
    - Decodable

    In most cases, these protocols are used to work with JSON, but in addition they are also used to save data to disk, transfer over the network, etc. Encodable is used to convert Swift data structures to JSON objects; Decodable, on the contrary, helps convert JSON objects to Swift data models. The Codable protocol combines the previous two and is their typealias:

    typealiasCodable = Encodable & Decodable


    To comply with these protocols, the data types must implement the following methods:

    Encodable
    encode (to :) - encodes the data model into the specified encoder type

    Decodable
    init (from :) - initializes the data model from the provided decoder

    Codable
    encode (to :)
    init (from :)

    A simple example of use

    Now let's consider a simple example of using Codable , since it implements both Encodable and Decodable , then using this example, you can immediately see the full functionality of the protocols. Suppose we have the simplest JSON data structure:

    {
           "title": "Nike shoes",
           "price": 10.5,
           "quantity": 1
    }


    The data model for working with this JSON will be as follows:
    structProduct: Codable{
      var title:Stringvar price:Doublevar quantity:IntenumCodingKeys: String, CodingKey{
        case title
        case price
        case quantity
      }
      funcencode(to encoder: Encoder)throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encode(price, forKey: .price)
        try container.encode(quantity, forKey: .quantity)
      }
      init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        price = try container.decode(Double.self, forKey: .price)
        quantity = try container.decode(Int.self, forKey: .quantity)
      }
    }
    


    Both necessary methods are implemented, the enumeration is also described to determine the list of encoding / decoding fields. In fact, the recording can be greatly simplified, because Codable supports the autogeneration of the methods encode (to :) and init (from :), as well as the necessary enumeration. That is, in this case, you can write the structure as follows:

    structProduct: Codable{
      var title:Stringvar price:Doublevar quantity:Int
    }


    Extremely simple and minimalist. The only thing you should not forget is that it’s impossible to use such a concise record if:

    - the structure of your data model differs from the one you want to encode / decode

    - you may need to encode / decode additional properties besides the properties of your data model

    - some properties of your data models may not support the Codable protocol. In this case, you will need to convert them from / to the Codable protocol

    - if the variable names in the data model and the field names in the container do not coincide with you.

    As we have already considered the simplest definition of the data model, you should give a small example of its practical use:

    So, in one line, you can parse the server response in JSON format:
    let product: Product = try! JSONDecoder().decode(Product.self, for: data) 


    And the following code, on the contrary, will create a JSON object from the data model:
    let productObject = Product(title: "Cheese", price: 10.5, quantity: 1)
    let encodedData = try? JSONEncoder().encode(productObject)


    Everything is very convenient and fast. Having correctly described the data models and making them Codable , you can literally in one line encode / decode the data. But we considered the simplest data model containing a small number of fields of a simple type. Consider potential problems:

    Not all fields in the data model are Codable.

    In order for your data model to be able to implement the Codable protocol , all model fields must support this protocol. By default, the Codable protocol supports the following data types: String, Int, Double, Data, URL . Also Codable support Array, Dictionary, Optional , but only in case they contain Codable types. If some properties of the data model do not correspond to Codable , then they must be brought to it.

    structPet: Codable{
        var name: Stringvar age: Intvar type: PetTypeenumCodingKeys: String, CodingKey{
            case name
            case age
            case type
        }
        init(from decoder: Decoder) throws {
        	.
        	.
        	.
        }
        funcencode(to encoder: Encoder)throws {
        	.
        	.
        	.
        }
    }


    If we use a custom type in our Codable data model , such as PetType , for example , and want to encode / decode it, then it must also implement its own init and encode too.

    The data model does not correspond to the JSON fields.

    If for example 3 fields are defined in your data model, and 5 fields come to the JSON object, 2 of which are additional to those 3, then nothing will change in the parsing, you will simply get your 3 fields from those 5. If the opposite situation occurs and at least one data model field is missing in the JSON object, then a run-time error will occur.
    If some fields in the JSON object may be optional and periodically absent, then in this case it is necessary to make them optional:

    classProduct: Codable {var id: Intvar productTypeId: Int?
        var art: String
        var title: String
        var description: String
        var price: Doublevar currencyId: Int?
        var brandId: Intvar brand: Brand?
    }


    Using more complex JSON structures.

    Often the server's response is an array of entities, that is, you request, for example, a list of stores and get the answer in the form:

    {
        "items": [
            {
                "id": 1,
                "title": "Тест видео",
                "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw",
                "created_at": 1497868174,
                "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg"
            },
            {
                "id": 2,
                "title": "Тест видео 2",
                "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8",
                "created_at": 1525952040,
                "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg"
            }
        ]
    }
    

    In this case, you can record and decode it simply as an array of Shop entities .

    structShopListResponse: Decodable{
        enumCodingKeys: String, CodingKey{
            case items
        }
        let items: [Shop]
    }


    In this example, the automatic function init will work , but if you want to write the decoding yourself, you will need to specify the type to be decoded as an array:

    self.items = try container.decode([Shop].self, forKey: .items)


    The structure of the Shop accordingly must also implement the Decodable protocol .

    structShop: Decodable{
        var id: Int?var title: String?var address: String?var shortAddress: String?var createdAt: Date?enumCodingKeys: String, CodingKey{
            case id
            case title
            case address
            case shortAddress = "short_address"case createdAt = "created_at"
        }
         init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
             self.id = try? container.decode(Int.self, forKey: .id)
             self.title = try? container.decode(String.self, forKey: .title)
             self.address = try? container.decode(String.self, forKey: .address)
             self.shortAddress = try? container.decode(String.self, forKey: .shortAddress)
             self.createdAt = try? container.decode(Date.self, forKey: .createdAt)
        }
    }


    Parsing this array of elements will look like this:

    let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data)


    Thus, you can easily work with arrays of data models and use them inside other models.

    Date format

    In this example there is another nuance, here we first encountered the use of the Date type . When using this type, problems with date encoding are possible and usually this issue is consistent with the backend. The default format is .deferToDate :

    struct MyDate : Encodable {
        letdate: Date
    }
    let myDate = MyDate(date: Date())
    try! encoder.encode(foo)


    myDate will look like this:
    {
      "date" : 519751611.12542897
    }


    If we need to use, for example, the .iso8601 format , then we can easily change the format using the dateEncodingStrategy property :

    encoder.dateEncodingStrategy = .iso8601


    Now the date will look like this:

    {
      "date" : "2017-06-21T15:29:32Z"
    }
    

    You can also use a custom date format or even write your own date decoder using the following formatting options:

    .formatted (DateFormatter) - your own date decoder format
    .custom ((Date, Encoder) throws -> Void) - create your own date decoding format

    Parsing nested objects

    We have already considered how you can use data models inside other models, but sometimes you need to parse JSON fields that belong to other fields without using a separate data model. The problem will be clearer if we consider it by example. We have the following JSON:

    {
        "id": 349,
        "art": "M0470500",
        "title": "Крем-уход Vichy 50 мл",
        "ratings": {
            "average_rating": 4.1034,
            "votes_count": 29
        }
    }


    We need to parse the “average” and “votes_count” fields , this can be solved in two ways, either create a Ratings data model with two fields and store data in it, or you can use nestedContainer . The first case we have already discussed, and the use of the second will be as follows:

    classProduct: Decodable{
        var id: Intvar art: String?var title: String?var votesCount: Intvar averageRating: DoubleenumCodingKeys: String, CodingKey{
            case id
            case art
            case title
            case ratings
        }
    	enumRatingsCodingKeys: String, CodingKey{
            case votesCount = "votes_count"case averageRating = "average_rating"
        }
    	requiredinit(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try container.decode(Int.self, forKey: .id)
            self.art = try? container.decode(String.self, forKey: .art)
            self.title = try? container.decode(String.self, forKey: .title)
            // Nested ratingslet ratingsContainer = try container.nestedContainer(keyedBy: RatingsCodingKeys.self, forKey: .ratings)
            self.votesCount = try ratingsContainer.decode(Int.self, forKey: .votesCount)
            self.averageRating = try ratingsContainer.decode(Double.self, forKey: .averageRating)
        }
    }


    That is, this problem is solved by creating another additional container using the nestedContainter and then parsing it. This option is useful if the number of nested fields is not so large, otherwise it is better to use an additional data model.

    Mismatch of JSON field names and data model properties

    If you pay attention to how the enumerations are defined in our data models, you can see that the enumeration elements are sometimes assigned a string that changes the default value, for example:

    enumRatingsCodingKeys: String, CodingKey{
            case votesCount = "votes_count"case averageRating = "average_rating"
        }


    This is done in order to properly match the names of model variables and JSON fields. This is usually required for fields whose name consists of several words, and in JSON they are separated by underscores. In principle, such an addition of an enumeration is the most popular and looks easy, but even in this case, Apple came up with a more elegant solution. This problem can be solved in a single line using keyDecodingStrategy . This feature appeared in Swift 4.1.

    Suppose you have a JSON of the form:

    let jsonString = """
    [
        {
            "name": "MacBook Pro",
            "screen_size": 15,
            "cpu_count": 4
        },
        {
            "name": "iMac Pro",
            "screen_size": 27,
            "cpu_count": 18
        }
    ]
    """
    let jsonData = Data(jsonString.utf8)


    Create a data model for it:

    structMac: Codable{
        var name: Stringvar screenSize: Intvar cpuCount: Int
    }
    

    The variables in the model are written in accordance with the agreement, begin with a lowercase letter, and then each word begins with a capital (the so-called camelCase ). But in JSON, the fields are written with underscores (called snake_case ). Now, in order for the parsing to succeed, we need to either define an enumeration in the data model, in which we will match the names of the JSON fields with the names of the variables, or we will get a runtime error. But now it is possible to simply define keyDecodingStrategy

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    do {
        let macs = try decoder.decode([Mac].self, from: jsonData)
    } catch {
        print(error.localizedDescription)
    }


    For the encode function, you can respectively use the inverse transform:

    encoder.keyEncodingStrategy = .convertToSnakeCase


    It is also possible to customize keyDecodingStrategy using the following closure:

    let jsonDecoder = JSONDecoder()
    jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey inlet key = keys.last!.stringValue.split(separator: "-").joined()
    	return PersonKey(stringValue: String(key))!
    }


    This entry, for example, allows the use of the delimiter "-" for JSON. JSON example used:

    {
    "first-Name": "Taylor",
    "last-Name": "Swift",
    "age": 28
    } 
    

    In this way, the additional definition of enumeration can often be avoided.

    Error handling

    When parsing JSON and when converting data from one format to another, errors are inevitable, so let's consider the options for handling different types of errors. The following types of errors are possible during decoding:

    • DecodingError.dataCorrupted (DecodingError.Context) - the data is corrupted. It usually means that the data you are trying to decode does not match the expected format, for example, instead of the expected JSON, you get a completely different format.
    • DecodingError.keyNotFound (CodingKey, DecodingError.Context) - The required field was not found. Means that the field you were expecting is missing
    • DecodingError.typeMismatch (Any.Type, DecodingError.Context) is a type mismatch. When the data type in the model does not match the type of the field received
    • DecodingError.valueNotFound (Any.Type, DecodingError.Context) - missing value for a specific field. The field that you defined in the data model could not be initialized, probably in the received data this field is nil. This error only happens with non-optional fields, if the field does not have to be important, remember to make it optional.


    When encoding the same data, the following error is possible:

    EncodingError.invalidValue (Any.Type, DecodingError.Context) - the data model could not be converted to a specific format.

    Example of error handling during JSON parsing:

    do {
                        let decoder = JSONDecoder()
                        _ = try decoder.decode(businessReviewResponse.self, from: data)
                    } catchDecodingError.dataCorrupted(let context) {
                        print(DecodingError.dataCorrupted(context))
                    } catchDecodingError.keyNotFound(let key, let context) {
                        print(DecodingError.keyNotFound(key,context))
                    } catchDecodingError.typeMismatch(let type, let context) {
                        print(DecodingError.typeMismatch(type,context))
                    } catchDecodingError.valueNotFound(let value, let context) {
                        print(DecodingError.valueNotFound(value,context))
                    } catchlet error{
                        print(error)
                    }


    Error handling is certainly better to make a separate function, but here, for clarity, error analysis goes along with parsing. For example, the error output when there is no value for the “product” field will look like this:

    image

    Comparison of Codable and NSCoding

    Of course, the Codable protocol is a big step forward in the encoding / decoding of data, but before it existed the NSCoding protocol. Let's try to compare them and see what advantages Codable has:

    • When using the NSCoding protocol , an object must necessarily be a subclass of NSObject , which automatically implies that our data model must be a class. In Codable , there is no need for inheritance, respectively, the data model can be both a class, and a struct and enum .
    • If you need separate encoding and decoding functions, such as in the case of parsing JSON data received via an API, you can use only one Decodable protocol . That is, there is no need to implement the sometimes unnecessary methods init or encode .
    • Codable can automatically generate the required init and encode methods , as well as the optional CodingKeys enumeration . This, of course, only works if you have simple fields in the data structure, otherwise, additional customization is required. In most cases, especially for basic data structures, you can use automatic generation, especially if you override keyDecodingStrategy , this is convenient and reduces some of the extra code.


    The Codable, Decodable and Encodable protocols allowed us to take another step towards the convenience of data conversion, new, more flexible parsing tools appeared, the amount of code was reduced, part of the conversion processes were automated. Protocols are natively implemented in Swift 4 and provide an opportunity to reduce the use of third-party libraries, such as SwiftyJSON , while maintaining ease of use. Protocols also provide an opportunity to properly organize the structure of the code, highlighting the data models and methods for working with them into separate modules.

    Also popular now: