September 15, 2016

On Apple's take on how to handle JSON with Swift 3

A couple of days ago, Apple published this post in which they describe some ways one can parse JSON using core languages of Swift 3.

Weird thing, they suggest that it is OK to have a type download JSON by itself:

You can create a type method on the Restaurant structure that translates a query method parameter into a corresponding request object and sends the HTTP request to the web service. This code would also be responsible for handling the response, deserializing the JSON data, creating Restaurant objects from each of the extracted dictionaries in the "results" array, and asynchronously returning them in a completion handler.

Please don't do this. Having a type inflate itself from the network is just a cry for trouble.

One thing I did like of Apple's post is that they put all of their custom code on an extension of the type they're trying to add JSON support to. What I didn't like, though, is that they're relying in initializers that decide wether the type can be inflated or not.

I really don't like that.

For me, the correct approach is to have a type method that validates wether the JSON is valid or not, and then return a type instance from that:

extension Restaurant {  
    static func with(json: [String:Any]) -> Restaurant? {
        // Verify that the JSON contains the correct values, etc.
    }
}

You could either return nil there, or just throw a custom error with the key that's missing, as they do in their example.

Swift is a language that pushes us to think functionally, and in terms of values. The static method on the type is the correct approach, I think. It also feels cleaner.

In case you want to read my full take on how I think JSON should be handled with Swift 3, you can read this post.


February 11, 2016

How I deal with JSON in Swift

Edit: I've updated this post to Swift 3's syntax. The original post with Swift 2.X syntax can be found here.


It seems that the new hot thing these days is open sourcing your Swift JSON parsing library.

Lyft, Big Nerd Ranch, Thoughtbot, to name a few, have their own drop-in solutions for handling JSON with Swift available for us to integrate in our projects. Other libraries like SwiftyJSON and json-swift, ObjectMapper are there for us, 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.

Before we start

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 that can be overworked.

Also, I don't use any of the libraries that I mentioned earlier to parse JSON in any of my apps. My method, which I'm about to show you, might seem a lot more verbose (hint hint it is) — but bear with me.

That said, if you just want to parse some JSON right now, stop reading and use any of the libraries linked earlier (I'd go with SwiftyJSON or Freddy, from BNR). If you want to learn how to handle JSON in Swift, read along.


As a side note: I'm writing a book — subscribe now to get a 50% off discount code when the book launches late October.


Groundwork

Is there a way to decode JSON using native APIs on Cocoa/CocoaTouch? Yep:

do {  
    let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
} catch { }

At this point, the constant json's type is Any. Typically, you'd cast json to a dictionary of some type (maybe an Array, depending on the API you're trying to consume):

do {  
    let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers) as? [String:Any]
} catch { }

Its our job now to extract data from that dictionary, and make sure 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 a chapter from Chris Eidhof's approach, let's define the following helper functions (updated for Swift 3, with the new operator declaration syntax and @discardableResult):

func flatten<A>(x: A??) -> A? {  
    if let y = x { return y }
    return nil
}

infix operator >>>=  
@discardableResult
public func >>>= <A, B> (optional: A?, f: (A) -> B?) -> B? {  
    return flatten(x: optional.map(f))
}

To explain briefly:

  • The flatten(x:) 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."

Then, by defining these next methods on a Dictionary extension, we can extract data from JSON structures in a type-safe manner. These are the building blocks of what I'm trying to do here.

extension Dictionary where Key: ExpressibleByStringLiteral, Value: Any {  
    public func p3_number(key: Key) -> NSNumber? {
        return self[key] >>>= { $0 as? NSNumber }
    }

    public func p3_int(key: Key) -> Int? {
        return self.p3_number(key: key).map { $0.intValue }
    }

    public func p3_float(key: Key) -> Float? {
        return self.p3_number(key: key).map { $0.floatValue }
    }

    public func p3_double(key: Key) -> Double? {
        return self.p3_number(key: key).map { $0.doubleValue }
    }

    public func p3_string(key: Key) -> String? {
        return self[key] >>>= { $0 as? String }
    }

    public func p3_bool(key: Key) -> Bool? {
        return self.p3_number(key: key).map { $0.boolValue }
    }
}

If we had the following JSON structure:

{
   name: "Oscar Swanros",
   age: 22
}

To get the age property, we can now do:

guard let age = json.p3_int(key: "age") else {  
    return
}

A lot cleaner than having to cast each member every time.

The fun part

The next step is to define a single protocol named JSONParselable:

public protocol JSONParselable {  
    static func with(json: [String:Any]) -> Self?
}

JSONParselable basically indicates that a given type can be inflated using JSON data. It requires a static function on the type that returns an instance of itself.

The types that can be inflated from a JSON structure need to conform to this protocol. The implementation is rather simple.

Say we have an API with the following JSON response:

{
  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:

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:

// Review.swift
extension Review: JSONParselable {  
    static func with(json: [String:Any]) -> Review? {
        guard
            let comment = json.p3_string(key: "comment"),
            reviewer    = json.p3_string(key: "reviewer")
            else {
                return nil
                // A valid Review always has a comment and a
                // reviewer.
            }

        return Review(
            comment: comment,
            reviewer: reviewer
        )
    }
}

Then...

// Author.swift
extension Author: JSONParselable {  
    static func with(json: [String:Any]) -> Author? {
        guard
            let name   = json.p3_string(key: "name"),
            websiteURL = json.p3_string(key: "website_url")
            else {
                return nil
            }

        // Since the twitterURL property is optional,
        // I can just call json.p3_string(key:) and pass that value to the
        // initializer. All good.
        return Author(
            name: name,
            websiteURL: websiteURL,
            twitterURL: json.p3_string(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. (Apple recommends another approach, which I'm not in favor of. Read my comments here.
  2. Up until this point, I haven't touched nested objects at all.

Next, I need to parse the main Book object. Here's how I do it:

// Book.swift
extension Book {  
    static func with(json: [String:Any]) -> Book? {
        // #1
        guard
            let id            = json.p3_int(key: "id"),
            let title         = json.p3_string(key: "title"),
            let numberOfPages = json.p3_int(key: "number_of_pages")
            else {
                return nil
            }

        // #2
        let authorsDicts = json["authors"] as? [[String:Any]]
        let reviewsDicts = json["reviews"] as? [[String:Any]]

        func sanitizedAuthors(dicts: [[String:Any]]?) -> [Author] {
            guard let dicts = dicts else {
                return [Author]()
            }

            return dicts.flatMap { Author.with($0) }
        }

        func sanitizedReviews(dicts: [[String:Any]]?) -> [Review] {
            guard let dicts = dicts else {
                return [Review]()
            }

            return dicts.flatMap { Review.with($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 Authors and Reviews.

The final implementation would look like this:

// Omitting networking code for brevity

func dataTaskFinished(data: Data) {  
    do {
        guard
            let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any],
            let book = Book.with(json: 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 you call me crazy:

  • The end result 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:

  • There are 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, 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 Strings?

Using my method, they can actually be URL objects!

struct Author {  
    let name: String
    let websiteURL: URL
    let twitterURL: URL?

    init(
        name: String,
        websiteURL: URL,
        twitterURL: URL? = nil
    ) {
        self.name       = name
        self.websiteURL = websiteURL
        self.twitterURL = twitterURL
    }
}

extension Author: JSONParselable {  
    static func with(json: [String:Any]) -> Author? {
        guard
            let name              = json.p3_string(key: "name"),
            let websiteURLString    = json.p3_string(key: "website_url"),
            let websiteURL          = URL(string: websiteURLString)
            else {
                return nil
            }

        let twitterURLString = json.p3_string(key: "twitter_url")

        return Author(
            name: name,
            websiteURL: websiteURL,
            twitterURL: URL(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. You can reach me as @Swanros on Twitter.