-
Notifications
You must be signed in to change notification settings - Fork 119
Subscripting
Subscripting instances of JSON
is accomplished in JSONSubscripting.swift
. This file provides an extension on JSON
to implement two subscript
s: one for an Int
index and one for a String
key. These allow you to subscript instances of JSON
in a way that feels intuitive to working with JSON data.
JSONSubscripting
also comprises other functionality that enable the convenient and safe retrieval of data from JSON
instances. For example, Freddy
provides methods to retrieve data from JSON
instances that return optionals or fallback values if the given path into the JSON
does not yield the expected data. See the sections on Optional Unpacking and Unpacking with Fallback respectively for more detail.
Freddy
has two subscripts: one for an Int
index, and one for a String
key. You can use these to access data within a JSON
instance. These subscripts work just like any other Swift subscript.
Recall the JSON described in the README.
let data = getSomeData()
do {
let json = try JSON(data: data)
let jobs = json["jobs"]
} catch {
// do something with the error
}
The line, let jobs = json["jobs"]
, uses a String
subscript to access the "jobs"
key within json
. jobs
is of type JSON?
set to the .array
case of the JSON
enum. This instance's case has an associated value of [JSON]
, which will all be of type JSON.string
.
Obviously, working with this sort of data can be a little tedious. jobs
is an optional, and its associated value encompasses several layers of JSON
instances. There has to be a better way.
Freddy
uses a protocol called JSONPathType
to make it easier to work with instances of JSON
. We have already seen an example in the README. let success = try json.getBool(at: "success")
, where "success"
refers to a path into json
where we expect to find a Bool
.
Let's revisit the example above to see how we can use this pattern here. Say you want to grab the job "teacher"
from the array of "jobs"
in json
. You could use the subscripts...
let data = getSomeData()
do {
let json = try JSON(data: data)
let teacher = json["jobs"]?[0]
} catch {
// do something with the error
}
teacher
is an optional whose value is the String
"teacher"
. This process looks a little tedious. Let's use a JSONPathType
.
First, let's take a look at the JSONPathType
protocol.
public protocol JSONPathType {
func value(in dictionary: [String : JSON]) throws -> JSON
func value(in array: [JSON]) throws -> JSON
}
A JSONPathType
is a protocol with two functions: one for finding a JSON
value within a dictionary, and another for finding a JSON
value in an array. Both are throws
ing functions.
These methods are given default implementations in a protocol extension.
extension JSONPathType {
public func value(in dictionary: [String : JSON]) throws -> JSON {
throw JSON.Error.UnexpectedSubscript(type: Self.self)
}
public func value(in array: [JSON]) throws -> JSON {
throw JSON.Error.UnexpectedSubscript(type: Self.self)
}
}
These default implementations both throw an .UnexpectedSubscript
error. The inference to make here is that only types conforming to JSONPathType
will be able to retrieve a value from a dictionary or an array. All other calls will simply throw
the .UnexpectedSubscript
error.
Freddy
extends the String
and Int
types to conform to JSONPathType
.
extension String: JSONPathType {
public func value(in dictionary: [String : JSON]) throws -> JSON {
guard let next = dictionary[self] else {
throw JSON.Error.KeyNotFound(key: self)
}
return next
}
}
extension Int: JSONPathType {
public func value(in array: [JSON]) throws -> JSON {
guard case array.indices = self else {
throw JSON.Error.IndexOutOfBounds(index: self)
}
return array[self]
}
}
The method value(in dictionary:)
method takes a dictionary of type [String: JSON]
and returns the result of attempting to subscript this dictionary with self
, which will be an instance of String
. If successful, then the method returns an instance of JSON
. If unsuccessful, then the method will throw
the error .KeyNotFound
with self
as the associated value.
The method value(in array:)
takes an array of type [JSON]
. If self
is within the range of valid indices for the array, then this array is subscripted by self
, which should be an Int
, and an instance of JSON
is returned. If this is not the case, then the error .IndexOutOfBounds
is thrown.
To understand how a JSONPathType
is used, let's look at the method declaration of getString(at:)
.
public func getString(at path: JSONPathType...) throws -> String
The getString(at:)
method has one variadic parameter that takes a list of zero or more comma separated JSONPathType
s that describe a path to a String
within a JSON
instance.
Thus, we can replace let teacher = json["jobs"]?[0]
with this:
do {
let json = try JSON(data: data)
let teacher = try json.getString(at: "jobs", 0)
// Do something with `teacher`
} catch {
// Do something with `error`
}
Recall that getString(at:)
throws
, which means you must call it with try
. So, instead of the return being String?
, you get a String
back if there is no error.
Thus, this syntax is safe and convenient. As you might expect, Freddy
provides getInt(at:)
and getDouble(at:)
methods in addition to the getString(at:)
and getBool(at:)
methods that you have already seen.
But what if a JSONPathType
describes a path into a JSON
instance that leads to an Array
or Dictionary
? Freddy
has you covered with:
public func getArray(at path: JSONPathType...) throws -> [JSON]
public func getDictionary(at path: JSONPathType...) throws -> [String: JSON]
Here is how you would use getArray(at:)
with the "jobs"
key.
do {
let json = try JSON(data: data)
let jobs = try json.getArray(at: "jobs")
// Do something with `jobs`
} catch {
// Do something with `error`
}
Here, jobs
is of type [JSON]
, where each element will be a JSON.String
.
This is nice, but it would be better if we could get the elements within "jobs"
as the String
instances we know them to be.
You can!
do {
let json = try JSON(data: data)
let jobs = try json.decodedArray(at: "jobs", type: String)
// Do something with `jobs`
} catch {
// Do something with `error`
}
The method decodedArray(at:type:)
has two parameters. The first takes a variable list of JSONPathType
s, and the second takes a type that conforms to JSONDecodable
. decodedArray(at:type:)
returns an Array
of whose elements also conform to JSONDecodable
. Thus, the second parameter supplies the type that the elements within the json
are decoded into for the return.
Freddy
provides extensions on Swift's basic data types to conform to JSONDecodable
, which means jobs
is of type [String]
in the above example.
As you might expect, you can have any type that you create to conform to JSONDecodable
and it will work beautifully with decodedArray(at:type:)
. See the wiki page for JSONDecodable
for more information.
The section above shows how you can use decodedArray(at:type:)
to find an array of data within a JSON
instance and return the anticipated Array
of a given type. But what if you just want one element within an array within the JSON? Use decode(at:type:)
?
Consider trying to get an instance of Person
from the example we have been working with from the README.
do {
let json = try JSON(data: data)
let matt = try json.decode(at: "people", 0, type: Person.self)
// Do something with `matt`
} catch {
// Do something with `error`
}
As with decodedArray(at:type:)
above, decode(at:type:)
takes two parameters. The first allows you to describe the path to the data of interest within a JSON
, and the second parameter specifies the type you would like to decode the JSON
into. The argument passed to this parameter must conform to JSONDecodable
.
Thus, the call above to decode(at:type:)
yields an instance of Person
, which is assigned to the constant matt
.
Freddy
provides a number of methods to create instances of JSON
or models that conform to JSONDecodable
, but what if what you are looking for in your JSON is not present? For example, what if you need to subscript the JSON for a key that you are not absolutely sure will be present at runtime? Typically, this would lead to a crash if the key is not present. Freddy
has a solution for this problem: use optional unpacking.
Let's refer back to our decode(at:type:)
example above. Freddy
has another version of this method that produces an optional if anything goes awry: decode(at:alongPath:type:)
.
The first parameter takes a variable list of JSONPathType
s. The second parameter, alongPath
, is a SubscriptingOptions
OptionSet
that dictates whether or not missing keys or index out of bounds errors are treated as nil
. The last parameter lists the type that is expected at the path within the JSON.
Let's look at an example of using this version of decode
when the supplied path is incorrect.
do {
let json = try JSON(data: data)
let matt = try json.decode(at: "peope", 0, alongPath: .MissingKeyBecomesNil, type: Person.self)
// Do something with `matt`
} catch {
// Do something with `error`
}
We pass MissingKeyBecomesNil
as the argument to the parameter alongPath
, which means that path errors will be treated as optionals. Since the supplied key -- "peope"
-- will not be found within the JSON, matt
is of type Person?
set to the value of nil
.
Optional unpacking versions are available for all of the methods reviewed above.
Sometimes you will want to provide a fallback value when querying JSON for a given path. That is, if there is no value at the path you provide, you would rather fallback to a default value than throw an error or deal with an optional.
Freddy
supplies fallback versions of all of the methods we have covered thus far. As an example, we can use decode(at:or:)
to default to a provided value if the path does not yield what we expect.
do {
let json = try JSON(data: data)
let matt = try json.decode(at: "peope", 0, or: Person(name: "Matt", age: 32, spouse: true))
matt.name
} catch {
// Do something with `error`
}
As above, the key "peope"
is not found within the JSON. In this case, the argument supplied to the or
parameter is used to construct the fallback value that will be assigned to matt
. or
takes an @autoclsoure
that creates an instance of some type that conforms to JSONDecodable
. Thus, we are able to create an instance of Person
using its memberwise initializer.
Created by Big Nerd Ranch 2015