FromJSONObject is a protocol adopted by Swift types that can be instantiated
from a dictionary of the sort produced by NSJSONSerialization
-- and that
produce an informative error when they can't.
struct Person {
let name: String
let age: Int
var description: String {
return "\(name), a \(age)-year-old"
}
}
extension Person: FromJSONObject { /* ... */ }
Person.fromJSONObject(["name": "Joe", "age": 12])
// Joe, a 12-year-old
Person.fromJSONObject(["name": "Dave", "age": "just a number"])
// error: expected number for key "age" (README.md, line 43); got NSString
FromJSONObject is also a module that makes adopting the FromJSONObject protocol easy, declarative, and fun. The module supports things like:
- Optional properties
- Properties which are themselves FromJSONObject
- Properties which are collections
- Conditional properties for enums or class clusters
- Keypaths
A typical implementation of FromJSONObject for Person
is:
extension Person: FromJSONObject {
static func fromJSONObject(o: NSDictionary) -> Result<Person> {
o.need("name") >>- { name in
o.need("age") >>- { age in
return success(
Person(name: name, age: age))
}}
}
}
Invoking need("age")
declares that the key "age"
is required -- i.e., that
fromJSONObject
produces an error if o["age"] == nil
.
It is inferred from the parameter types of Person()
that o["name"]
and
o["age"]
should be a string and a number respectively. If they're not,
fromJSONObject
produces an error.
Any error produced by fromJSONObject
will point to a line number in your
implementation of the method:
Person.fromJSONObject(["name": "Keanu"]
// error: expected key "age" (README.md, line 43)
Line 43 is where we declared o.need("age")
.
Optional properties are declared using the want
method:
struct Issue: FromJSONObject {
let assigneeID: Int?
var description: String { return "Assigned to \(assigneeID)" }
static func fromJSONObject(o: NSDictionary) -> Result<Issue> {
o.want("assignee_id") >>- { assigneeID in
return success(
Issue(assigneeID: assigneeID))
}
}
}
If the key is absent from the dictionary, then assigneeID
is nil
:
Issue.fromJSONObject([])
// Assigned to nil
An error is still produced if the key is present with an unexpected value type:
Issue.fromJSONObject(["assignee_id": "123"])
// error: expected number for key "assignee_id" (README.md, line 78); got NSString
Beware that NSJSONSerialization
translates JSON's null
to NSNull()
. A key
with value NSNull()
is not equivalent to an absent key:
Issue.fromJSONObject(["assignee_id": NSNull()])
// error: expected number for key "assignee_id" (README.md, line 78); got NSNull
If we want to interpret nulls as missing values, we can use the allowNull
parameter:
//...
o.want("assignee_id", allowNull: true) >>- { assigneeID in
// ...
Now the null doesn't trigger an error:
Issue.fromJSONObject(["assignee_id": NSNull()])
// Assigned to nil
You may wish not only to allow nulls, but to treat them as distinct from missing
values. You can accompish this with wantEither
.
static func fromJSONObject(o: NSDictionary) -> Result<Task> {
o.wantEither("assignee_id")>- { newAssigneeID in
return success(
IssueUpdate(newAssigneeID: newAssigneeID)
}
}
JSON objects often contain other JSON objects. FromJSONObject can be composed from other FromJSONObject types.
struct User: FromJSONObject {
let id: Int
let name: String
static func fromJSONObject(o: NSDictionary) -> Result<User> {
o.need("id") >>- { id in
o.need("name") >>- { name in
return success(
User(id: id, name: name))
}}
}
}
struct Task: FromJSONObject {
let creator: User
static func fromJSONObject(o: NSDictionary) -> Result<Task> {
o.needObject("creator") >>- { creator in
return success(
Task(creator: creator))
}
}
}
Errors originating from a key in a nested object reflect that context:
Task.fromJSONObject(["creator": ["id": 123]])
// error: in object "creator" (README.md, line 163), expected key "name" (README.md, line 152)