Skip to content

Instantly share code, notes, and snippets.

@brow
Created November 21, 2014 06:30
Show Gist options
  • Save brow/c9488d6e6d0e736f546f to your computer and use it in GitHub Desktop.
Save brow/c9488d6e6d0e736f546f to your computer and use it in GitHub Desktop.
FromJSONObject

FromJSONObject

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

Adopting FromJSONObject

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

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

allowNull

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

Either<NSNull, T>

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)
  }
}

Object properties

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment