Codable
As an iOS developer, your work may require you to retrieve data from servers and upload data back to them. This data may contain links to images, descriptions, subscription data, or information about whether the user was successfully signed in or logged in.
For this purpose, we generally use the widely accepted JSON (JavaScript Object Notation) format. JSON is a lightweight data-interchange format that is easy for devs to read and write and simple for machines to parse and generate.
In this article, we’ll investigate simplifying JSON parsing in Swift using Codable
. We’ll also review several practical JSON use cases. From simple JSON to nested polymorphic JSON, soon you’ll be able to use Codable
to more easily parse and generate JSON in your mobile app!
Let’s get started!
Jump ahead:
Before we dive into the use cases, it’s important to recognize that JSON can be categorized into two different structures:
{ "first_name": "Rudrank", "last_name": "Riyam" }
{ "qualifications": [ { "name": "high_school", "passed": true }, { "name": "bachelors", "passed": true }, { "name": "masters", "passed": false } ] }
Swift offers several protocols to help us change the way data is represented.
Decodable
protocolDecodable
is a type that can decode itself from an external representation.
protocol Decodable
Here’s a simple JSON example:
{ "name":"rudrank" }
This JSON can be decoded into a structure that conforms to the Decodable
protocol:
struct Information: Decodable { let name: String }
This protocol has one required initializer, init(from: Decoder)
, that has a default implementation. When we are decoding a JSON in a custom way, we can use this initializer to provide our implementation.
Encodable
protocolEncodable
is a type that can encode itself to an external representation.
protocol Encodable
Using the example above, we can use the same structure and conform to the Encodable
protocol to encode an object of Information
into a JSON:
struct Information: Encodable { let name: String }
This protocol has one required method, func encode(to: Encoder)
, with a default implementation. When we encode a JSON in a custom way, we can use this method to provide our implementation.
Codable
protocolTo easily parse JSON and similar formats, like XML or Property Lists (PLISTs), we can take advantage of the Codable
protocol introduced in Swift 4. Codable
is a typealias
for Decodable
and Encodable
protocols, meaning it provides a new name to an existing type:
typealias Codable = Decodable & Encodable
Codable
is a type that can convert itself into and out of an external representation, where the representation is JSON or a similar format.
The Swift standard library contains types like String
, Int
, Double
, Date
, Data
, and URL
that already conform to Codable
. If we create a custom struct
, enum
, or class
, we can conform it to the Codable
protocol and use the already existing types without implementing any methods.
Apple has provided us with two classes for decoding and encoding JSON objects, JSONDecoder
and JSONEncoder
.
JSONDecoder
classThe JSONDecoder
class decodes instances of a data type from JSON objects. The data type must conform to the Decodable
protocol; it can be either predefined types like String
, Double
, or Date
or custom classes, enumerations, or structures.
With this class, we will generally use the decode_:from:)
method, which has the following definition:
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
This method takes the JSON response in the form of Data
type and a generic type that conforms to the Decodable
protocol. After decoding the data
into the given generic type, the method returns the type.
Let’s consider again the earlier example with the object of Information
that conforms to the Decodable
protocol and corresponding JSON:
struct Information: Decodable { let name: String } let informationData = """ { "name":"rudrank" } """.data(using: .utf8)!
All we have to do is create an instance of the JSONDecoder
and call the decode_:from:)
method:
let decoder = JSONDecoder() let information = try decoder.decode(Information.self, from: informationData) print(information.name) // Prints "rudrank"
JSONEncoder
classWe use JSONEncoder
to encode, rather than decode, instances of a data type as JSON objects. With this class, we primarily use the encode(_:)
method that has the following definition:
func encode<T>(_ value: T) throws -> Data where T : Encodable
This method takes a generic type that conforms to the Encodable
protocol, and returns Data
after encoding the type into data.
Let’s take another look at the above example:
struct Information: Encodable { let name: String } let information = Information(name: "rudrank")
We have a structure Information
that we want to encode into a JSON response. Encoding a custom structure or class this way is useful when you are doing a POST request and want to add the JSON as the resource of the body of the request.
We create an instance of JSONDecoder()
and use the encode(_:)
method to encode the information
constant:
let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(information)
To visualize how the JSON response looks, we create a string from the encoded data and print it:
print(String(data: data, encoding: .utf8)!) // Prints { "name":"rudrank" }
This article will mainly focus on decoding and simplifying the parsing of the JSON response into structures that we can easily use in an iOS app. Once you have a good understanding of these decoding examples, encoding the type into JSON should be a trivial task.
The examples used in the article, from different Apple Music API endpoints, represent practical, real-world use cases.
Let’s start with a simple example that contains only one JSON object. If we search for suggestions using the Apple Music API, we get the following response:
{ "kind":"terms", "searchTerm":"the weeknd", "displayTerm":"the weeknd" }
Creating a struct
for this object is simple. We require the properties kind
, searchTerm
, and displayTerm
that are of the type String
:
struct Suggestion: Codable { let kind: String let searchTerm: String let displayTerm: String }
To decode the JSON into a struct
, we’ll use JSONDecoder()
:
let suggestionResponse = """ { "kind":"terms", "searchTerm":"the weeknd", "displayTerm":"the weeknd" } """ let suggestionData = Data(suggestionResponse.utf8) let suggestion = try JSONDecoder().decode(Suggestion.self, from: suggestionData) print(suggestion) // MARK: - OUTPUT Suggestion(kind: "terms", searchTerm: "the weeknd", displayTerm: "the weeknd")
Decoding a single JSON object is easy, but what if a key contains an object of its own, like a nested structure? To address this scenario, we’ll create another struct
for the nested object, and the key property will be the type of that object.
if we search the Apple Music API genres endpoint, we get the following response:
{ "id":"20", "type":"genres", "attributes":{ "parentId":"34", "name":"Alternative", "parentName":"Music" } }
You can see that attributes
contain an object of their own, an example of a nested JSON.
To decode this, we’ll create two different struct
, one for the main object and one for the nested object:
struct Genre: Codable { let id: String let type: String let attributes: Attributes } struct Attributes: Codable { let parentId: String let name: String let parentName: String }
To decode the nested JSON into a struct
, we’ll use JSONDecoder()
:
let genreResponse = """ { "id":"20", "type":"genres", "attributes":{ "parentId":"34", "name":"Alternative", "parentName":"Music" } } """ let genreData = Data(genreResponse.utf8) let genre = try JSONDecoder().decode(Genre.self, from: genreData) print(genre) // MARK: - OUTPUT Genre(id: "20", type: "genres", attributes: Attributes(parentId: "34", name: "Alternative", parentName: "Music"))
In the above example, we used parentId
as the variable’s name. What if we wanted to use parentID
instead? Or, suppose we preferred to use parent
as a variable name instead of parentName
?
To address these cases, we can create alternate keys while decoding. We’ll create an enum
of the type String
containing all the cases as the keys and conform to the CodingKey
protocol:
struct Attributes: Codable { let parentID: String let name: String let parent: String enum CodingKeys: String, CodingKey { case parentID = "parentId" case name case parent = "parentName" } }
Then, we’ll write our custom initializer to decode the data:
extension Attributes { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) parentID = try values.decode(String.self, forKey: .parentID) name = try values.decode(String.self, forKey: .name) parent = try values.decode(String.self, forKey: .parent) } }
Considering the above example again, let’s suppose a particular genre does not have a parent genre. In this case, the JSON object will have a null value for those keys, or those keys will be missing in the object.
In our example, there would be no keys for parentName
and parentId
:
{ "id":"34", "type":"genres", "attributes":{ "name":"Music", } }
If you have been working with Swift, you are familiar with the Optional type. We can use the same concept here and update the structure Attributes
to accept optional values for the parentName
and parentId
properties:
struct Attributes: Codable { let parentId: String? let name: String let parentName: String? }
However, this approach can get cumbersome for cases with many structures. Instead of unwrapping optional values for each structure, we can write a custom initializer for the decoder and take advantage of the decodeIfPresent
method:
struct Attributes: Codable { let parentID: String let name: String let parent: String enum CodingKeys: String, CodingKey { case parentID = "parentId" case name case parent = "parentName" } } extension Attributes { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) parentID = try values.decodeIfPresent(String.self, forKey: .parentID) ?? "" name = try values.decode(String.self, forKey: .name) parent = try values.decodeIfPresent(String.self, forKey: .parent) ?? "" } }
With this technique, we can specify a placeholder value that will be provided if the response returns a missing key or a null value.
The JSON response may contain many different objects, but it could also contain an array of objects.
Let’s go back to the simple example where we have a Suggestion
struct and extend it to an array of suggestions
:
{ "results":{ "suggestions":[ { "kind":"terms", "searchTerm":"the weeknd", "displayTerm":"the weeknd" }, { "kind":"terms", "searchTerm":"the weeknd & swedish house mafia", "displayTerm":"the weeknd & swedish house mafia" }, { "kind":"terms", "searchTerm":"weeknd nigth", "displayTerm":"weeknd nigth" }, { "kind":"terms", "searchTerm":"weeknd warriorz", "displayTerm":"weeknd warriorz" }, { "kind":"terms", "searchTerm":"yeyo weeknd", "displayTerm":"yeyo weeknd" } ] } }
You can see that the key suggestions
contains an array of objects of type Suggestion
, instead of a single Suggestion
. To create structures for a JSON array, we’ll create a property of the type [Suggestion]
:
struct Suggestions: Codable { let results: Results struct Results: Codable { let suggestions: [Suggestion] } } struct Suggestion: Codable { let kind: String let searchTerm: String let displayTerm: String }
We’ll decode the data in the same manner as the previous examples, except that now the struct
has a property that is an array:
let suggestionsResponse = """ { "results":{ "suggestions":[ { "kind":"terms", "searchTerm":"the weeknd", "displayTerm":"the weeknd" } /// rest of the JSON ] } } """ let suggestionsData = Data(suggestionsResponse.utf8) let suggestions = try JSONDecoder().decode(Suggestions.self, from: suggestionsData) print(suggestions) // MARK: - OUTPUT Suggestions(results: Suggestions.Results(suggestions: [ Suggestion(kind: "terms", searchTerm: "the weeknd", displayTerm: "the weeknd"), Suggestion(kind: "terms", searchTerm: "the weeknd & swedish house mafia", displayTerm: "the weeknd & swedish house mafia"), Suggestion(kind: "terms", searchTerm: "weeknd nigth", displayTerm: "weeknd nigth"), Suggestion(kind: "terms", searchTerm: "weeknd warriorz", displayTerm: "weeknd warriorz"), Suggestion(kind: "terms", searchTerm: "yeyo weeknd", displayTerm: "yeyo weeknd") ])))
By now you should have an understanding of how to work with single JSON, nested JSON, and JSON arrays. You’ll probably encounter a combination of all of these in your work as an iOS developer. The nested structure may go down three to four levels deep in the hierarchy, and the deepest structure may contain an array of objects.
Let’s take a look at an example of a stripped JSON for a personal recommendation response. It contains an array of objects under the data
key, and each object contains different nested objects, like attributes
, artwork
, and title
.
The nextUpdateDate
method contains a date in the form of a string, so we can simplify the use case by decoding it as a Date
in Swift:
{ "data":[ { "id":"6-27s5hU6azhJY", "type":"personal-recommendation", "attributes":{ "resourceTypes":[ "playlists" ], "artwork":{ "width":1200, "height":1200, "url":"https://sampleimage.com/link/to/the/image.jpg" }, "nextUpdateDate":"2022-04-16T19:00:00Z", "kind":"music-recommendations", "isGroupRecommendation":false, "title":{ "stringForDisplay":"Made for You" } } } ] }
Now, we’ll create the following structures:
struct PersonalRecommendation: Codable { let recommendations: [Recommendation] enum CodingKeys: String, CodingKey { case recommendations = "data" } } struct Recommendation: Codable { let id, type: String let href: URL let attributes: Attributes } struct Attributes: Codable { let resourceTypes: [String] let artwork: Artwork let nextUpdate: Date let kind: String let isGroupRecommendation: Bool let title: String struct Artwork: Codable { let width, height: Int let url: String } struct Title: Codable { let stringForDisplay: String } enum CodingKeys: String, CodingKey { case resourceTypes, artwork case nextUpdate = "nextUpdateDate" case kind, isGroupRecommendation case title } enum TitleCodingKeys: String, CodingKey { case stringForDisplay } }
Here are the Attributes
that we’ll need to decode:
extension Attributes { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) resourceTypes = try values.decode([String].self, forKey: .resourceTypes) artwork = try values.decode(Artwork.self, forKey: .artwork) nextUpdate = try values.decode(Date.self, forKey: .nextUpdate) kind = try values.decode(String.self, forKey: .kind) isGroupRecommendation = try values.decode(Bool.self, forKey: .isGroupRecommendation) let titleValues = try values.nestedContainer(keyedBy: TitleCodingKeys.self, forKey: .title) title = try titleValues.decode(String.self, forKey: .stringForDisplay) } }
You can see how we are decoding the Attributes
differently from the JSON response. The response contains the title
as an object like this:
"title":{ "stringForDisplay":"Made for You" }
Instead of creating a variable stringForDisplay
, we directly use the title
in the Attributes
structure. To achieve that, we create an enumeration TitleCodingKeys
that holds the different keys under title
object:
enum TitleCodingKeys: String, CodingKey { case stringForDisplay }
Then, during the decoding process, we get the nested container of the object for the key title
. Based on the nested container, we decode the stringForDisplay
to the variable title
:
let titleValues = try values.nestedContainer(keyedBy: TitleCodingKeys.self, forKey: .title) title = try titleValues.decode(String.self, forKey: .stringForDisplay)
So, instead of using attributes.title.stringForDisplay
, we can now directly use it as attributes.title
!
The final step is to decode the response. As mentioned earlier, the response contains a date. To decode it correctly, we take advantage of the dataDecodingStrategy
of JSONDecoder()
. We set the property to iso8601
, an international standard covering the worldwide exchange and communication of date and time-related data:
let recommendationResponse = """ { "data":[ { "id":"6-27s5hU6azhJY", "type":"personal-recommendation", "href": "/v1/me/recommendations/6-27s5hU6azhJY", "attributes":{ "resourceTypes":[ "playlists" ], "artwork":{ "width":1200, "height":1200, "url":"https://sampleimage.com/link/to/the/image.jpg" }, "nextUpdateDate":"2022-04-16T19:00:00Z", "kind":"music-recommendations", "isGroupRecommendation":false, "title":{ "stringForDisplay":"Made for You" } } } ] } """ let recommendationData = Data(recommendationResponse.utf8) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let personalRecommendation = try decoder.decode(PersonalRecommendation.self, from: recommendationData) print(personalRecommendation)
Another use case that you may encounter is where the response has different objects and a few of them have the same key but others have more dynamic keys. Extending on the array example, let’s say that the response now also contains the top results.
You can see that kind
is a common key in both the objects but in the first case, we have searchTerm
and displayTerm
keys, but the second object has a nested object content
:
{ "results":{ "suggestions":[ { "kind":"terms", "searchTerm":"the weeknd", "displayTerm":"the weeknd" }, { "kind":"topResults", "content":{ "id":"1488408568", "type":"songs", "attributes":{ "artistName":"The Weeknd", "url":"https://music.apple.com/in/album/blinding-lights/1488408555?i=1488408568", "genreNames":[ "R&B/Soul", "Music" ], "durationInMillis":201570, "releaseDate":"2019-11-29", "name":"Blinding Lights", "hasLyrics":true, "albumName":"Blinding Lights - Single" } } } ] } }
One approach to decoding and parsing this JSON is to have a non-optional kind
property, as well as three optional properties: searchTerm
, displayTerm
, and content
. But, when dealing with optionals, an array of this type of data will quickly get cumbersome and will not be scalable. Can we do better? Let’s find out!
We start with a similar structure for the top-level hierarchy, but we create two different struct
for terms and top results, TermSuggestion
and TopResultsSuggestion
:
struct TermSuggestion: Codable { let kind: String let searchTerm: String let displayTerm: String } struct TopResultsSuggestion: Codable { let kind: String let content: Content } struct Content: Codable { let id, type: String let attributes: Attributes } struct Attributes: Codable { let name, albumName, artistName: String let url: URL? let genres: [String] let duration: Int let releaseDate: Date let hasLyrics: Bool } extension Attributes { enum CodingKeys: String, CodingKey { case name, albumName, artistName, url case genres = "genreNames" case duration = "durationInMillis" case releaseDate, hasLyrics } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decode(String.self, forKey: .name) albumName = try values.decode(String.self, forKey: .albumName) artistName = try values.decode(String.self, forKey: .artistName) url = URL(string: try values.decode(String.self, forKey: .url)) genres = try values.decode([String].self, forKey: .genres) duration = try values.decode(Int.self, forKey: .duration) releaseDate = try values.decode(Date.self, forKey: .releaseDate) hasLyrics = try values.decode(Bool.self, forKey: .hasLyrics) } }
Note how the Attributes
structure has an optional URL
. In the initializer, we decode it as a URL from the given URL string.
We can create an enumeration, SuggestionKind
, that has the coding key for kind
to help us distinguish between the two use cases, terms
and topResults
:
enum SuggestionKind: Codable { case terms(TermSuggestion) case topResults(TopResultsSuggestion) enum CodingKeys: String, CodingKey { case kind } enum SuggestionsKind: String, Codable { case terms case topResults } public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) let kind = try values.decode(SuggestionsKind.self, forKey: .kind) switch kind { case .terms: let termSuggestion = try TermSuggestion(from: decoder) self = .terms(termSuggestion) case .topResults: let topResultsSuggestion = try TopResultsSuggestion(from: decoder) self = .topResults(topResultsSuggestion) } } func encode(to encoder: Encoder) throws { var values = encoder.container(keyedBy: CodingKeys.self) switch self { case .terms(let termSuggestion): try values.encode(termSuggestion, forKey: .kind) case .topResults(let topResultsSuggestion): try values.encode(topResultsSuggestion, forKey: .kind) } } }
After checking the kind
key, we switch over the values to create the two cases of objects and associate them according to the relevant codable structures. In this case, these two codable structures are TermSuggestion
and TopResultsSuggestion
. We can take advantage of the existing decoder
as both the structures are already Decodable
. The Decoder
does the heavy lifting for us and decodes the data.
For example, if the value of kind
is terms
, the decoder
takes the object corresponding to the terms
from the JSON and decodes it to TermSuggestion
. Then, you set the decoded value to self
.
For decoding, we see that there is a custom date associated with releaseDate
. So, we take advantage of the property dateDecodingStrategy
of JSONDecoder
and pass a custom data formatter, like so:
let suggestionsData = Data(suggestionsResponse.utf8) let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(dateFormatter) let suggestions = try decoder.decode(Suggestions.self, from: suggestionsData) print(suggestions) // MARK: - OUTPUT Suggestions(results: Suggestions.Results(suggestions: [ SuggestionKind.terms(TermSuggestion(kind: "terms", searchTerm: "the weeknd", displayTerm: "the weeknd")), SuggestionKind.topResults(TopResultsSuggestion(kind: "topResults", content: Content(id: "1488408568", type: "songs", attributes: Attributes(name: "Blinding Lights", albumName: "Blinding Lights - Single", artistName: "The Weeknd", url: Optional(https://music.apple.com/in/album/blinding-lights/1488408555?i=1488408568), genres: ["R&B/Soul", "Music"], duration: 201570, releaseDate: 2019-11-28 18:30:00 +0000, hasLyrics: true)))) ]))
Parsing and generating JSON has been simplified in recent years through the use of Decodable
and Encodable
protocols combined with the JSONDecoder
and JSONEncoder
classes, respectively.
In this article, we covered several use cases for simplifying JSON parsing in Swift using the Codable
protocol.
After reviewing the practical examples in the article, ranging from basic use cases to those with complex JSON objects and those with dynamic objects, I hope you feel ready to easily parse the JSON in your iOS app!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.