This article will explain how to use Decodable
` in Swift
` with a few use cases.
Before we get started, a good tool I want to introduce to make your JSON
result looks pretty is JSONFormatter.
Section A: General Base Case
Given this url, which will return a JSON file contains a list of dog image urls. Example could be looked like:
{
"message": [
"https://images.dog.ceo/breeds/hound-afghan/n02088094_4352.jpg",
"https://images.dog.ceo/breeds/hound-afghan/n02088094_5559.jpg",
"https://images.dog.ceo/breeds/hound-afghan/n02088094_7894.jpg"
],
"status": "success"
}
It’s easy to see there are two keys in this structure, message
and status
. Then, our Swift
code could be looked like:
struct BreedResponse: Decodable {
let message: [String]
let status: String
}let result = try? JSONDecoder().decode(BreedResponse.self, from: data)
In the code above, a JSONDecoder
instance here will try to decode the Data
instance received.
Section B: General Case with Custom Key
In some scenarios, the JSON
key’s name does not do a perfect job representing the meaning of the content/value, so, to make your code more readable, developer would possible write a meaning name, which could be long and different from the JSON
key name. In this case, developer need to associate your model’s property with the JSON
key name.
So, our model could be modified with better named properties.
struct BreedResponse: Decodable {
let imageURLList: [String]
let status: String
enum CodingKeys: String, CodingKey {
case imageURLList = "message"
case status
}
}
In the code above, if we need to map a name to another, we need to specify the name in JSON
structure. Otherwise, just list it as another case in enum
.
Now, the model’s property has more meaningful name, which indicates the what exactly result we are expecting here. This will improve the readability for you and other coworkers.
Section C: Nested Structure
In the real world case, a well structured JSON
result could provide enough information the app is trying to request, while explain the relationship structure of the models as well. JSON
result could have nested structures to present abundant information, so the app would not make multiple URL
request to reach the final result. So, the developer would expect a nested JSON
result in any time.
A good example of JSON
result:
{
"reference": "John 3:16",
"verses": [
{
"book_id": "JHN",
"book_name": "John",
"chapter": 3,
"verse": 16,
"text": "\nFor God so loved the world, that he gave his one and only Son, that whoever believes in him should not perish, but have eternal life.\n\n"
}
],
"text": "\nFor God so loved the world, that he gave his one and only Son, that whoever believes in him should not perish, but have eternal life.\n\n",
"translation_id": "web",
"translation_name": "World English Bible",
"translation_note": "Public Domain"
}
The way we decode sample result like this would be:
struct VerseInfo: Decodable {
let bookID: String
let bookName: String
let chapter: Int
let verse: Int
let content: String
enum CodingKeys: String, CodingKey {
case bookID = "book_id"
case bookName = "book_name"
case chapter
case verse
case content = "text"
}
}struct BibleContent: Decodable {
let reference: String
let verses: [VerseInfo]
let content: Stringenum CodingKeys: String, CodingKey {
case reference
case verses
case content = "text"
}
}let result = try? JSONDecoder().decode(BibleContent.self, from: data)
Something interesting here deserves to be mentioned:
VerseInfo
needs to conformDecodable
, because every property ofBibleContent
needs to conform toDecodable
to makeBibleContent
decodable.- In the model definition, we don’t need to define all the properties for every key in the
JSON
structure. It’s reasonably to take only important/useful values from that.
Section D: Advanced Nested Structure
Other uncommon case could be: With this url link could be looked like:
{
"message": {
"affenpinscher": [],
"australian": [
"shepherd"
],
"buhund": [
"norwegian"
],
"bulldog": [
"boston",
"english",
"french"
],
"stbernard": [],
"terrier": [
"american",
"australian",
"bedlington",
"border",
"cairn",
"dandie",
"fox",
"irish",
"kerryblue",
"lakeland",
"norfolk",
"norwich",
"patterdale",
"russell",
"scottish",
"sealyham",
"silky",
"tibetan",
"toy",
"welsh",
"westhighland",
"wheaten",
"yorkshire"
]
},
"status": "success"
}
This example is much trickier than the usual nested result, since the nested level does a non-repetitive key as the name. In this case, we need to customize how to decode the data in details.
// Model to define Breed
struct Breed: Decodable {
let name: String
let subBreeds: [String]
}// Model to define the JSON result.
struct AllBreedResponse: Decodable {
let breedList: [Breed]
let status: String enum CodingKeys: String, CodingKey {
case breedList = "message"
case status
}
}// Extension to implement the customization of JSON Parsing.
extension AllBreedResponse {
init(from decoder: Decoder) throws {
// 1. This will be the root container.
let values = try decoder.container(keyedBy: CodingKeys.self)
// 2. This will be the common way to decode a standard swift library type.
let status = try values.decode(String.self, forKey: .status)
let allBreeds = try values.decode([String : [String]].self, forKey: .breedList)
// 3, we finish the initialization.
self.status = status
self.breedList = allBreeds.map({ key, values in
Breed(name: key, subBreeds: values)
})
}
}
In the code above,
- Get the root container from decoder. It could be treated as handler for the root of your
JSON
result. - Decode data of standard Swift library type.
- Wrap up the initializer by assigning converted/mapped values to every properties needs to be assigned.
The tricky part of this parsing is do some manual parsing during the whole parsing process.
One more thing to mention here is: using KeyedDecodingContainer
could handle more complicated nested case easily.
Conclusion
In the above, we have demonstrated how to parse the JSON result from simple case to nested case, from straight answer to the answer with customization needed.
Hopefully this article with solid example could help you understand it better.