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 NSStringFromJSONObject 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 nilAn 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 NSStringBeware 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 NSNullIf 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 nilYou 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)