JSON parsing in Swift is a common thing to do. Almost every app decodes JSON to show data in a visualized way. Parsing JSON is definitely one of the basics you should learn as an iOS developer.
Decoding JSON in Swift is quite easy and does not require any external dependencies. The basic APIs that come with Swift will be enough to do the job, so let’s dive in!
The basics of JSON decoding
It’s good to start with the basics to let you understand how JSON parsing in Swift works. Let’s take the following example of a SwiftLee blog post:
{
"title": "Optionals in Swift explained: 5 things you should know",
"url": "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/",
"category": "swift",
"views": 47093
}
We can easily decode this by making use of the Decodable
protocol:
struct BlogPost: Decodable {
enum Category: String, Decodable {
case swift, combine, debugging, xcode
}
let title: String
let url: URL
let category: Category
let views: Int
}
We defined a Category
enum that also conforms to the Decodable
protocol. All the properties match the names from our defined JSON example. Every type that conforms to the Decodable protocol automatically converts. This means that you can also use your own custom-defined Decodable
types as a property.
By making use of a JSONDecoder
we can make JSON parsing possible:
let JSON = """
{
"title": "Optionals in Swift explained: 5 things you should know",
"url": "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/",
"category": "swift",
"views": 47093
}
"""
let jsonData = JSON.data(using: .utf8)!
let blogPost: BlogPost = try! JSONDecoder().decode(BlogPost.self, from: jsonData)
print(blogPost.title) // Prints: "Optionals in Swift explained: 5 things you should know"
Although this might give the impression that JSON parsing is easy, it all comes down to the edge cases. Luckily enough, Swift is capable enough to handle those as well.
It’s not required to define each property
It’s good to know that you’re not required to define each property that comes with your JSON. This means that the following struct would’ve worked as well:
struct BlogPost: Decodable {
let title: String
}
This is great, as it could be that you’re adding new keys to the JSON backend response after you’ve already released a version of your app. If it wouldn’t work like this, you could easily break old versions.
Optionals and JSON decoding
It could be that you’re unsure whether a JSON key is returned or a value will be set. In this case, you can define a Swift property as optional, and the JSONDecoder
will take care of the rest.
struct BlogPost: Decodable {
let title: String
/// Define a key as optional if it can be returned as `nil` or if it does not always exist in the JSON.
let subtitle: String?
}
I recommend reading my article Optionals in Swift explained: 5 things you should know if you’re new to optionals.
Decoding JSON arrays in Swift
Decoding a JSON array in Swift is almost as easy as decoding a single JSON object. Take the following JSON example:
[{
"title": "Optionals in Swift explained: 5 things you should know",
"url": "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/"
},
{
"title": "EXC_BAD_ACCESS crash error: Understanding and solving it",
"url": "https://www.avanderlee.com/swift/exc-bad-access-crash/"
},
{
"title": "Thread Sanitizer explained: Data Races in Swift",
"url": "https://www.avanderlee.com/swift/thread-sanitizer-data-races/"
}]
We can parse this list of blog posts by defining the decodable type as [BlogPost].self
:
struct BlogPost: Decodable {
let title: String
let url: URL
}
let blogPosts: [BlogPost] = try! JSONDecoder().decode([BlogPost].self, from: jsonData)
print(blogPosts.count) // Prints: 3
Mapping JSON keys to custom property names
JSON parsing isn’t always as easy as copying over the same keys into a struct. It’s pretty common that you like to define different property names when mapping the JSON.
Taking the previous JSON example, it could be that we would like to name url
as htmlLink
in our JSON model. We can create this mapping by defining a custom CodingKeys
enum:
struct BlogPost: Decodable {
enum Category: String, Decodable {
case swift, combine, debugging, xcode
}
enum CodingKeys: String, CodingKey {
case title, category, views
// Map the JSON key "url" to the Swift property name "htmlLink"
case htmlLink = "url"
}
let title: String
let htmlLink: URL
let category: Category
let views: Int
}
let blogPost: BlogPost = try! JSONDecoder().decode(BlogPost.self, from: jsonData)
print(blogPost.htmlLink) // Prints: "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/"
As you can see, we defined a custom mapping to convert the JSON key url
into the Swift property name htmlLink
.
As we’re not changing the name of title, category, and views, we can keep this case the same. We do have to include those keys as the JSONDecoder
will switch to our defined mapping for all defined properties. If we don’t do it, we will run into the following error:
Type ‘BlogPost’ does not conform to protocol ‘Decodable’
Conversion between camel case and snake case
A common reason to define custom mapping for keys is that your backend uses snake case for naming properties. In Swift, we’re mainly using camel case, which means that we start with a lowercase letter and then capitalize the first letter of subsequent words: htmlLink
or numberOfBlogPosts
. The same words in snake case look as follows: html_link
and number_of_blog_posts
.
Luckily, we don’t have to define a custom mapping for each key. Take the following example JSON of a blog:
{
"title": "A weekly Swift Blog on Xcode and iOS Development - SwiftLee",
"url": "https://www.avanderlee.com",
"total_visitors": 378483,
"number_of_posts": 47093
}
We can easily decode that JSON by setting the keyEncodingStrategy
of our decoder to .convertFromSnakeCase
:
struct Blog: Decodable {
let title: String
let url: URL
let totalVisitors: Int
let numberOfPosts: Int
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let blog: Blog = try! decoder.decode(Blog.self, from: jsonData)
print(blog.numberOfPosts) // Prints: 47093
That was super easy! This also works fine with custom-defined keys. So, if you would like to map url
to htmlLink
just like we did before, you can easily do that as follows:
struct Blog: Decodable {
enum CodingKeys: String, CodingKey {
case title, totalVisitors, numberOfPosts
case htmlLink = "url"
}
let title: String
let htmlLink: URL
let totalVisitors: Int
let numberOfPosts: Int
}
Decoding JSON dates with custom formats
Dates in JSON are defined as a String or time interval and require a conversion strategy. We can set such a strategy for our JSONDecoder
, just like we did for converting camel case to snake case.
Take the following JSON example of a blog post:
{
"title": "Optionals in Swift explained: 5 things you should know",
"date": "2019-10-21T09:15:00Z"
}
The date in this example is defined in the following format: yyyy-MM-dd'T'HH:mm:ss
. We need to create a custom DateFormatter
with this format and apply this to our decoder by setting the dateDecodingStrategy
to formatted
:
struct BlogPost: Decodable {
let title: String
let date: Date
}
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
dateFormatter.locale = Locale(identifier: "en_US")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let blogPost: BlogPost = try! decoder.decode(BlogPost.self, from: jsonData)
print(blogPost.date) // Prints: 2019-10-21 09:15:00 +0000
There are a few other strategies available to set:
deferredToDate
: Uses Apple’s date format that tracks the number of seconds and milliseconds since January 1st, 2001. This is mainly useful to use directly with Apple’s platforms.millisecondsSince1970
: This format tracks the number of seconds and milliseconds since January 1st of 1970 and is much more common.secondsSince1970
: Tracks the number of seconds since January 1st of 1970.iso8601
: Decodes the Date as an ISO-8601-formatted string (in RFC 3339 format).
Depending on how the API you’re using returns the dates, you can choose between those strategies.
Benefiting from AI to convert JSON into Swift
My number one in my personal top 5 AI code generation prompts is about parsing JSON into a Swift struct. It’s a straightforward conversion that AI can handle very well. The prompt would be like:
Can you write a Swift struct called Person for the following JSON [JSON]
And you’ll get a working code example out of it. Do note that AI often generates more than needed, including a custom coding key in all cases. Therefore, you’ll have to do some manual cleanup, like you always have to check AI coding results.
How to pretty print JSON in Swift
Printing JSON in a pretty format is useful during debugging, for example, when finding out what a backend has to return.
You can pretty print an encodable type using the following extension:
extension Encodable {
func prettyPrintJSON() {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let encodedData = try? encoder.encode(self) else {
print("Failed to encode data")
return
}
let prettyJSONString = String(decoding: encodedData, as: UTF8.self)
print(prettyJSONString)
}
}
For example, when using it on the Blog
struct that we defined earlier:
let blog: Blog = ...
blog.prettyPrintJSON()
// Prints:
// {
// "numberOfPosts" : 47093,
// "title" : "A weekly Swift Blog on Xcode and iOS Development - SwiftLee",
// "totalVisitors" : 378483,
// "url" : "https:\/\/www.avanderlee.com"
// }
If you want to pretty print a data response from a URL request that you’ve performed, you can use the following extension:
extension Data {
func prettyPrintJSON() {
guard let object = try? JSONSerialization.jsonObject(with: self) else {
print("Could not create JSON object from data")
return
}
guard let serializedData = try? JSONSerialization.data(
withJSONObject: object,
options: [.prettyPrinted, .sortedKeys]
) else {
print("Could not serialize JSON data")
return
}
let prettyJSONString = String(decoding: serializedData, as: UTF8.self)
print(prettyJSONString)
}
}
How to read a JSON response from a request in Swift
When actively implementing API requests, you’re likely looking for a way to inspect network traffic and read the returned JSON. By reading the JSON, you can properly convert the JSON into a Swift struct to work with. My recommended way of working is to use the technique described in this article to inspect network traffic in the Simulator: Inspect network traffic using the Xcode Simulator.
Once you have the JSON, you can use the techniques described in this article to convert it into a Swift value type.
Conclusion
Swift makes decoding JSON easy. There’s no need to use a custom library for JSON parsing as the default API brings everything we need, from custom key mapping to formatting dates.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!