Parsing and working with Codable in Swift 4



JSON format has become very popular, it is usually used for data transfer and query execution 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:

typealias Codable = Encodable & Decodable 


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

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

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

Codable
encode (to :)
init (from :)

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:
 struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(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:

 struct Product: Codable { var title:String var price:Double var quantity:Int } 


Extremely simple and minimalist. The only thing you should not forget is that this concise record will not work if:

- the structure of your data model is different 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 model 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 names of the fields in the container you do not match

Since we have already considered the simplest definition of a data model, it is worth giving 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 possible problems:

Not all fields in the data model are Codable.

In order for your data model 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 . Codable also supports Array, Dictionary, Optional , but only if they contain Codable types. If some properties of the data model do not correspond to Codable , then they must be brought to it.

 struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(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.

Data model does not match JSON fields

If, for example, 3 fields are defined in your data model, and in the JSON object, 5 fields come to you, 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 happens the situation and in the JSON object will be missing at least one field of the data model, then a runtime 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:

 class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var 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.

 struct ShopListResponse: Decodable { enum CodingKeys: 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 .

 struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: 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 { let date: 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) - its date decoder format
.custom ((Date, Encoder) throws -> Void) - create completely your own date decoding format

Parsing nested objects

We have already considered how to use data models inside other models, but sometimes it is required 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:

 class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(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 ratings let 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 enums are defined in our data models, you can see that the elements of the enumerations are sometimes assigned a string that changes the default value, for example:

 enum RatingsCodingKeys: 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:

 struct Mac: Codable { var name: String var screenSize: Int var 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 in let 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 processing

When parsing JSON and 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:



When encoding the same data, an error may occur:

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

An example of error handling when parsing JSON:

  do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let 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 in the absence of a value for the “product” field would look like this:

image

Codable versus NSCoding

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



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.

Source: https://habr.com/ru/post/414221/


All Articles