Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
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.
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:
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’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.
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’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.
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.
JSON
InstancesNow, 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 try
ing 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
.
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.
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.
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.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...