Search

Introducing Freddy, an Open-Source Framework for Parsing JSON in Swift

Matt Mathias

5 min read

Jan 30, 2016

iOS

Introducing Freddy, an Open-Source Framework for Parsing JSON in Swift

Parsing JSON can be tricky, but it shouldn’t be. We think parsing JSON should feel like writing regular Swift code.

That’s why we’re happy to introduce Freddy, a new open-source framework for parsing JSON in Swift 2. Freddy provides a native interface for parsing JSON.

In the battle of Freddy vs. JSON, who wins? We think it’s Freddy.

Who Needs Another JSON Framework?

There are already several JSON parsing solutions available in the Swift ecosystem, so why do Swift developers need another one?

In our survey of what was available, and in our own work, we were unsatisfied with the current options. Freddy is our solution, and seeks to:

  1. Maximize safety
  2. Be familiar using idiomatic style
  3. Provide speedy parsing

Freddy is Safe

The goal of Freddy is to transform JSON data into an instance of Freddy’s JSON type. JSON is an enumeration with a case for each data type described by the format.

public enum JSON {
    case Array([JSON])
    case Dictionary([Swift.String: JSON])
    case Double(Swift.Double)
    case Int(Swift.Int)
    case String(Swift.String)
    case Bool(Swift.Bool)
    case Null
}

Each case has an associated value, with the exception of .Null. Because JSON data can have a nested structure, the cases for .Array and .Dictionary have associated values that contain further instances of JSON.

The benefit of this enumeration is that if you have an instance of JSON, then you know you have some data to examine in the case’s associated value (with exception of .Null).
Freddy exposes several methods for safely retrieving values from JSON’s cases.

Freddy is Idiomatic

Freddy’s API focuses on providing an approach to parsing JSON that feels right in Swift. That means there are no strange custom operators. The framework uses optionals when it makes sense, and it utilizes Swift’s mechanism for error handling to provide additional safety.

Freddy also uses the good practices established by the Swift community and the standard library. For example, Freddy uses protocols and extensions to make the code more readable and modular.

Users of Freddy should be able to read through the source code and easily understand its architecture. We also hope that our approach will make it easy for fans of the framework to contribute to its future.

Freddy is Fast

Our initial solution fed NSData to NSJSONSerialization.JSONObjectWithData(_:options:) and then recursively switched through the returned AnyObject to create an instance of JSON. We found that parsing JSON in this manner can be slow for large data payloads, largely due to switching over, and casting between, the various types encapsulated by the AnyObject instance.

So we wrote our own parser that natively emits an instance of JSON directly. Our parser is much faster than the alternative described above. Check out the Wiki page for benchmarks and more information.

Freddy in Action

Freddy’s README provides a good introduction to the framework, and Freddy’s Wiki offers a lot of great information and examples. Let’s take a look at an example to get started.

Sample JSON

Consider some sample JSON:

{
    "messages": [
        {
            "content": "Here is some message.",
            "read": false
        },
        {
            "content": "More messages for you!",
            "read": true
        }
    ]
}

This JSON describes a user’s messages. The key "messages" has an array of dictionaries as its value. Each dictionary in the array is a message. For simplicity, a message just has two keys: one for content, and another for whether the message has been read.

Decoding JSON Instances

Now, let’s imagine that you want to parse this JSON into instances of a model type called Message. Here is how that type looks:

struct Message {
    let content: String
    let isRead: Bool
}

The question becomes: How do we decode the above JSON data into instances of Message? Freddy’s solution uses a protocol named JSONDecodable.

public protocool JSONDecodable {
    init(json: JSON) throws
}

JSONDecodable requires conforming types to implement an initializer that takes an instance of JSON.
Message needs to implement this initializer.

extension Message: JSONDecodable {
    public init(json: JSON) throws {
        content = try json.string("content")
        isRead = try json.bool("read")
    }
}

Since parsing JSON can be error prone, we must try to find the String associated with the key "content". The same is also true for trying to find the Bool associated with the key "read". Either of these calls may generate an error, and so this initializer is marked with throws.

Creating JSON

We know that we need pass an instance of JSON to Message’s implementation of init(json:). But how do we do that? We need to create an instance of JSON.

let data: NSData = getSomeData() // E.g., from a web service
do {
    let json = try JSON(data: data)
    let messages = try json.array("messages").map(Message.init)
} catch {
   // Handle errors
}

After we getSomeData()—a fictitious method that returns an instance of NSData—we can create an instance of JSON with its initializer: init(data:usingParser:). The second parameter, usingParser, has a default value that uses our custom JSONParser type. With json in hand, we can begin the work of finding our user’s messages.

The call json.array("messages") finds the array value for the key "messages" within the JSON instance. Essentially, we supply a path within the JSON"messages"—and get out a new JSON instance representing what was found at the path. In this case, we get a JSON array of dictionaries.

Next, we chain a call to map(Message.init) on that array to apply the JSONDecodable initializer to each element. We pass each of the JSON dictionaries to init(json:). Message’s implementation will grab the relevant data and assign it to the content and isRead properties. If everything goes as planned, messages will be of type [Message]—an array of the user’s messages.

If everything does not go as planned, then you will get an error telling you what went wrong. This helps you to debug your JSON, and will increase your confidence in your program at runtime.

Another Way

The call json.array("messages").map(Message.init) may feel clunky to you. Good news! Freddy provides an easier way.

let data: NSData = getSomeData() // E.g., from a web service
do {
    let json = try JSON(data: data)
    let messages = try json.arrayOf("messages", type: Message.self)
} catch {
   // Handle errors
}

Functionally, arrayOf(_:type:) behaves very similarly to what we saw above with array("messages").map(Message.init). The improvement here is that it does the same work (it produces an array of messages) with a more concise syntax.

Freddy in the Future

You can read more about the particulars of Freddy in the online documentation. We hope that our approach will make it easy for fans of the framework to contribute to its future.

Josh Justice

Reviewer Big Nerd Ranch

Josh Justice has worked as a developer since 2004 across backend, frontend, and native mobile platforms. Josh values creating maintainable systems via testing, refactoring, and evolutionary design, and mentoring others to do the same. He currently serves as the Web Platform Lead at Big Nerd Ranch.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News