JSON Parsing in Swift explained with code examples

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 basics 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 really simple:

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 really 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 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 not sure whether a JSON key is returned or whether 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?
}

Decoding JSON arrays in Swift

Decoding a JSON array in Swift is almost just 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 quite 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 wouldn’t do it, we would 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 the backend you’re using uses snake case for naming properties. In Swift, we’re mostly 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 enough, we don’t have to define a custom mapping for each defined 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 on 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 with 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 own data format that tracks the number of seconds and milliseconds since January 1st of 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 a lot more common to use.
  • secondsSince1970: Tracks the numbers 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.

Conclusion

Swift makes decoding JSON really 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!