
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-
DecodableIn 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:
Encodableencode (to :) - encodes the data model in the specified encoder type
Decodableinit (from :) - initializes the data model from the provided decoder
Codableencode (to :)
init (from :)
Simple example of useNow 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 matchSince 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 fieldsIf, 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 structuresOften 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 formatIn 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 objectsWe 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)
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 propertiesIf 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 processingWhen 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:
- 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, do not forget to make it optional.
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:
Codable versus NSCodingOf 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:
- 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.