Posted originally: 11 Feb 2016 @ 11:11 — http://swanros.com/how-i-deal-with-json-in-swift/
---
# How I deal with JSON in Swift
It seems that the new hot thing these days is open sourcing your Swift JSON parsing library.
[Lyft](https://github.com/lyft/mapper), [Big Nerd Ranch](https://www.bignerdranch.com/blog/introducing-freddy-an-open-source-framework-for-parsing-json-in-swift/), [Thoughtbot](https://robots.thoughtbot.com/efficient-json-in-swift-with-functional-concepts-and-generics), to name a few, have their own drop-in solutions for handling JSON with Swift available for you to integrate in your projects. Other libraries like [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) and [json-swift](https://github.com/owensd/json-swift), [ObjectMapper](https://github.com/Hearst-DD/ObjectMapper) are there too. All of them are great. All of them get the job done.
This post is not a tutorial on how to use those libraries, but just a description of how *I* deal with JSON in my Swift apps.
### Disclaimer
I'm pretty biased. Personally, I think that handling JSON in Swift, while challenging at first (specially if you're new to the language), can be a really simple task.
Also, I don't use any of the libraries that I mentioned earlier. My method might seem a lot more verbose (and it is), but bear with me.
If you just want to parse some JSON **now**, stop reading and use any of the libraries linked earlier (I'd go with [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) or [Freddy](https://www.bignerdranch.com/blog/introducing-freddy-an-open-source-framework-for-parsing-json-in-swift/), from BNR). If you want to ***learn*** how to handle JSON in Swift, read along.
Little plug right here: **[I wrote a book](http://iosbestpractices.com)** — subscribe now and get a 50% off discount code when the book launches late October.
### Groundwork
How can you decode JSON in Swift with Cocoa natively?
```swift
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers)
} catch { }
```
At this point, the constant `json`'s type is `AnyObject`. Typically, you'd cast `json` to a dictionary of some type (maybe an `Array`, depending on the API you're trying to consume):
```swift
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers) as? [String:AnyObject]
} catch { }
```
Its our job now to extract data from that dictionary, and make sure that we can build our Swift models in a type-safe manner.
Lets define what basic data types we can retrieve from a JSON structure:
1. Ints
2. Floats
3. Strings
4. Bools
> *Note*: Nested objects and arrays are just arrangements of the basic data types.
Taking chapter from [Chris Eidhof's approach](http://chris.eidhof.nl/posts/json-parsing-in-swift.html), I defined the following helper functions:
```swift
func flatten(x: A??) -> A? {
if let y = x { return y }
return nil
}
infix operator >>>= {}
func >>>= (optional: A?, f: A -> B?) -> B? {
return flatten(optional.map(f))
}
```
Briefly:
- The `flatten(_:)` function takes a double optional and removes one level of optional-*ness*.
- The custom operator `>>>=` takes an optional of type `A` to the left, and a function that takes an `A` as a parameter and returns an optional `B` to the right. Basically, it says "apply."
These next functions retrieve data from JSON structures in a type-safe manner, and they're the building blocks for what I'm trying to do here.
```swift
func number(input: [NSObject:AnyObject], key: String) -> NSNumber? {
return input[key] >>>= { $0 as? NSNumber }
}
func int(input: [NSObject:AnyObject], key: String) -> Int? {
return number(input, key: key).map { $0.integerValue }
}
func float(input: [NSObject:AnyObject], key: String) -> Float? {
return number(input, key: key).map { $0.floatValue }
}
func double(input: [NSObject:AnyObject], key: String) -> Double? {
return number(input, key: key).map { $0.doubleValue }
}
func string(input: [String:AnyObject], key: String) -> String? {
return input[key] >>>= { $0 as? String }
}
func bool(input: [String:AnyObject], key: String) -> Bool? {
return number(input, key: key).map { $0.boolValue }
}
```
If we had the following JSON structure:
```json
{
name: "Oscar Swanros",
age: 22
}
```
To get the `age` property, then:
```swift
guard let age = int(json, key: "age") else {
return
}
```
### The fun part
I defined a single protocol `JSONParselable`:
```swift
protocol JSONParselable {
static func withJSON(json: [String:AnyObject]) -> Self?
}
```
My types that can be inflated from a JSON structure need to conform to this protocol. The implementation is rather simple.
Say I'm trying to consume an API with the following JSON response:
```json
{
id: 289928,
title: "Freakonomics: A Rogue Economist Explores the Hidden Side of Everything",
number_of_pages: 320,
authors: [
{
name: "Steven D. Levitt",
website_url: "http://pricetheory.uchicago.edu/levitt/home.html",
twitter_url: "http://twitter.com/freakonomics"
},
{
name: "Stephen J. Dubner",
website_url: "http://stephenjdubner.com",
}
],
reviews: [
{
comment: "If Indiana Jones were an economist, he’d be Steven Levitt… Criticizing Freakonomics would be like criticizing a hot fudge sundae.",
reviewer: "Wall Street Journal"
},
{
comment: "The guy is interesting!",
reviewer: "Washington Post Book World"
},
{
comment: "Principles of economics are used to examine daily life in this fun read.",
reviewer: "Great Reads"
}
]
}
```
Working from the inside-out, these are the models that I'd define:
```swift
struct Review {
let comment: String
let reviewer: String
init(
comment: String,
reviewer: String
) {
self.comment = comment
self.reviewer = reviewer
}
}
struct Author {
let name: String
let websiteURL: String
let twitterURL: String?
// I defined twitterURL as optional because, judging from the sample
// JSON, it is a field that may or may not be present in
// the response. Ideally, this would be well defined
// on the documentation for the API I'm trying to consume.
init(
name: String,
websiteURL: String,
twitterURL: String? = nil
) {
self.name = name
self.websiteURL = websiteURL
self.twitterURL = twitterURL
}
}
struct Book {
let id: Int
let title: String
let numberOfPages: Int
let authors: [Author]
let reviews: [Review]
init(
id: Int,
title: String,
numberOfPages: Int,
authors: [Author],
reviews: [Review]
) {
self.id = id
self.title = title
self.numberOfPages = numberOfPages
self.authors = authors
self.reviews = reviews
}
}
```
Now I have models that map directly to my API definition, including proper handling of values that may not be in the response at all. Now, on to implementing the `JSONParselable` protocol.
I'll do one of the inner models first:
```swift
// Review.swift
extension Review: JSONParselable {
static func withJSON(json: [String:AnyObject]) -> Review? {
guard
let comment = string(json, key: "comment"),
reviewer = string(json, key: "reviewer")
else {
return nil
// A valid Review always has a comment and a
// reviewer.
}
return Review(
comment: comment,
reviewer: reviewer
)
}
}
```
Then...
```swift
// Author.swift
extension Author: JSONParselable {
static func withJSON(json: [String:AnyObject]) -> Author? {
guard
let name = string(json, key: "name"),
websiteURL = string(json, key: "website_url")
else {
return nil
}
// Since the twitterURL property is optional,
// I can just call string(_:key:) and pass that value to the
// initializer. All good.
return Author(
name: name,
websiteURL: websiteURL,
twitterURL: string(json, key: "twitter_url")
)
}
}
```
Attention:
1. Notice how the protocol implementation is done in an extension for each type. This to emphasize the notion that the actual type does not care about how it is created, as long as it is valid.
2. Up until this point, I haven't dealt with nested objects.
Next, I need to parse the main `Book` object. Here's how I do it:
```swift
// Book.swift
extension Book {
static func withJSON(json: [String:AnyObject]) -> Book? {
// #1
guard
let id = int(json, key: "id"),
title = string(json, key: "title"),
numberOfPages = int(json, key: "number_of_pages")
else {
return nil
}
// #2
let authorsDicts = json["authors"] as? [[String:AnyObject]]
let reviewsDicts = json["reviews"] as? [[String:AnyObject]]
func sanitizedAuthors(dicts: [[String:AnyObject]]?) -> [Author] {
guard let dicts = dicts else {
return [Author]()
}
return dicts.flatMap { Author.withJSON($0) }
}
func sanitizedReviews(dicts: [[String:AnyObject]]?) -> [Review] {
guard let dicts = dicts else {
return [Review]()
}
return dicts.flatMap { Review.withJSON($0) }
}
// #3
return Book(
id: id,
title: title,
numberOfPages: numberOfPages,
// #4
authors: sanitizedAuthors(authorsDicts),
reviews: sanitizedReviews(reviewsDicts)
)
}
}
```
Dissecting the last code listing:
- **#1**: Make sure that the required data is in the JSON dictionary, and extract it accordingly.
- **#2**: Extract the dictionaries for authors and reviews respectively.
- **#3**: Return a new instance of `Book` passing the **required** data first.
- **#4**: For `authors` and `reviews`, just `.flatMap` the array of dictionaries, and construct an array of only valid `Author`s and `Review`s.
The final implementation would look like this:
```swift
// Omitting networking code for brevity
func dataTaskFinishedWithData(data: NSData) {
do {
guard
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers) as? [String:AnyObject],
book = Book.withJSON(json)
else {
return
}
// #1
print(book)
} catch {
print(error)
}
}
```
At this point (#1), I have a fully-packaged, type-safe, valid instance of `Book`, with nested models, even, not only a type-safe `JSON` type that I still have to extract data from. 🎉
### Why do it this way
My method is (really) verbose, I know. But think about this few points before calling me crazy:
- The end result using is not a "type-safe-parsed JSON object," as most of the libraries out there give you, but a fully type-safe, valid model instance.
- While this may seem a lot of work, it lets you be more granular about how you inflate your models from data from a server.
- My method, at least to me, makes it easier to translate API documentation to code.
Other highlights:
- **No custom operators to deal with.** Yes, the `>>>=` operator was defined, but it is not required for the actual implementation of the JSON parsing ([Argo](https://github.com/thoughtbot/Argo), for instance, *requires* you to use custom operators to perform the actual parsing)
- **Modularity.** Dealing with nested objects kinda solves itself. If you make every nested model conform to `JSONParselable`, you can just
send the original JSON blob to the outermost object, and you're done.
### Extra!
Did you notice that `Author` has two properties that their name makes you think they're URLs but are actually `String`s?
Using my method, they can actually be `NSURL` objects!
```swift
struct Author {
let name: String
let websiteURL: NSURL
let twitterURL: NSURL?
init(
name: String,
websiteURL: NSURL,
twitterURL: NSURL? = nil
) {
self.name = name
self.websiteURL = websiteURL
self.twitterURL = twitterURL
}
}
extension Author: JSONParselable {
static func withJSON(json: [String:AnyObject]) -> Author? {
guard
let name = string(json, key: "name"),
websiteURLString = string(json, key: "website_url"),
websiteURL = NSURL(string: websiteURLString)
else {
return nil
}
let twitterURLString = string(json, key: "twitter_url")
return Author(
name: name,
websiteURL: websiteURL,
twitterURL: NSURL(string: twitterURLString)
)
}
}
```
So, there's that.
### Wrapping up
As I said at the beginning of this post, the JSON parsing libraries that are out there are really popular (because they work), and they can help you get from point A to point B really quick (if that's what you want).
My issue with those libraries is that they are too general-purpose for my taste. They are designed to work out of the box with any generic JSON response.
However, my issue is that my apps are not interacting with a generic API expecting generic JSON — they interact with a very specific API that outputs very specific JSON.
If my problem is really specific, why not solve it very specifically?
Have anything to add? [Let me know](mailto:oscar@swanros.com?subject=JSON%20with%20Swift%20comment). You can reach me as [@Swanros on Twitter](http://twitter.com/Swanros).