Build Your Own Swift JSONModel

Dec. 23, 2016 | Apple Inc. TW

Tai-Lun Tseng teaualune@gmail.com

What is JSONModel?

And what problem does it tempt to solve?

https://github.com/jsonmodel/jsonmodel

Transform JSON from/to Objective-C entity classes

{
  "id": 1,
  "name": "Lawrence",
  "friends": [
    {
      "id": 3,
      "name": "Michael"
    },
    {
      "id": 7,
      "name": "Davis"
    }
  ]
}
@interface User : JSONModel
@property (nonatomic) NSUInteger id;
@property (nonatomic) String *name;
@property (nonatomic) Optional<NSArray<User>> *friends;
@end

Code example

NSError *error;
User *user = [[User alloc] initWithString:jsonString error:&error];
NSDictionary *userDictionary = [user toDictionary];
String *stringifiedUser = [user toJSONString];

Why Use JSONModel?

It's natural!

Server & App Communication

  • Server: HTTP Server
  • App: Send HTTP Requests and Get Responses
  • Server: RESTful Server with JSON contents
  • App: Use AFNetworking/Alamofire and JSONModel

We need fast serialization/de-serialization for JSON

JSONModel Features

  • Contains its ownNSURLResponsewrapper
    • AddsContent-Type: application/jsonautomatically in request headers
    • Automatically de-serializes response body into Objective-C objects
  • Use objc_getAssociatedObject to get property names
  • Use NSKeyValueCoding to set JSON values into the entity object

Existing Swift Solutions

Let's see some popular building blocks that tackled this problem:

  • ObjectMapper
  • Argo
  • Unbox & Wrap

ObjectMapper

https://github.com/Hearst-DD/ObjectMapper
  • A popular mapper for Swift
  • In your entity model, implements ObjectMapper.Mappable protocol:
required init?(map: ObjectMapper.Map) {}
 
mutating func mapping(map: ObjectMapper.Map) {
  userId <- map["id"]
  name   <- map["name"]
}
  • Write mapping logics in the implemented method body and you're good to go!

Argo

https://github.com/thoughtbot/Argo
  • Leverage custom operators (heavily inspired by Haskell) from Runes and currying from Curry
  • In your entity model, implements Argo.Decodable protocol:
extension User: Decodable {
  static func decode(j: JSON) -> Decoded<User> {
    return curry(User.init)
      <^> j <|  "id"
      <*> j <|  "name"
      <*> j <|| "friends"
  }
}
  • Write mapping logics in the implemented method body and you're good to go!

Unbox & Wrap

https://github.com/JohnSundell/Unbox
https://github.com/JohnSundell/Wrap
  • Two libraries for decode/encode respectively
  • Wrap utilizes Swift Mirror API to encode objects automatically
  • To decode: in your entity model, implements Unbox.Unboxable protocol:
init(unboxer: Unboxer) throws {
  self.userId = try unboxer.unbox(key: "id")
  self.name   = try unboxer.unbox(key: "name")
}
  • Write mapping logics in the implemented method body and you're good to go!

See Common parts?

  • Write mapping logics in the implemented method body and you're good to go!
  • Write mapping logics in the implemented method body and you're good to go!

Why do we need to write additional mapping logics? Unlike JSONModel, which only requires defining our @property name well?

Before answering the question, let's see some case studies in other languages:

  • Android Programming
  • Golang
  • JavaScript

GSON

https://github.com/google/gson
  • Utilizes Reflection API to encode/decode entity objects automatically
  • Easy customization options with @annotation
public class User implements Serializable {
  private long id;
  @Expose
  @SerializedName("id")
  private long userId;
  @Expose
  @SerializedName("name")
  private String name;
  @Expose
  @SerializedName("friends")
  private List<User> friends;
}

Additional Showtime: Retrofit

https://github.com/square/retrofit
  • Declare RESTful HTTP endpoints via annotations in an interface
  • Great combination with GSON library
public interface APIEndpoint {
  @GET("/users")
  Call<List<User>> getUsers();
  @PUT("/users/{userId}")
  Call<User> updateUser(@Path("userId") String userId, @Body User user);
}

Golang

  • The struct tag feature of Golang plays nicely with JSON encoding/decoding:
type User struct {
  UserId  uint64 `json:"id"`
  Name    string `json:"name"`
}
 
func UpdateUserHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  user := schema.User{UserId: ps.ByName("userId")}
  decoder := json.NewDecoder(r.Body)
  if err := decoder.Decode(&user); err != nil {
    w.WriteHeader(http.StatusBadRequest)
    fmt.Fprintf(w, "request body error")
  } else { /* ... */ }
}

About JavaScript…

Since JSON (= Java Script Object Notation) comes from JavaScript, we don't need additional libraries:

user = JSON.parse(userStr);
userStr = JSON.stringify(user);
// user = { id: 1, name: 'Lawrence' }
// userStr = '{"id":1,"name":"Lawrence"}'

Back to Swift, what JSONModel alternative library features do we want?

1. Convenience

  • As less encode/decode codes as possible
  • Less code = less chance to get things wrong (typo, API change, etc.)
  • JSONModel is the most ideal form: define@propertyname and everything is set.

2. Customizable for non-default cases

  • Only write additional codes when mapping does not fit in default cases
  • Examples:
    • id(JSON) touserId(property name) mapping
    • Custom time formatting, e.g. Unix timestamp (JSON) toDateobjects
    • URL strings toNSURLobjects

3. Coupling and cohesion

  • We want less coupling with other libraries
  • For example, in ObjectMapper you must use ObjectMapper.Map instead of built-in NSDictionary to store raw JSON
  • Can we use different libraries together for server-app HTTP connections without writing cross-library adapters?

Some approaches to fulfill the requirements

  • Assume that we've used several libraries to build our API manager: networking, JSON handling, RESTful wrapper, etc.
  • Now we want to add an entity mapper that can cooperates other libraries well
  • Either choose one and write adapters, or write one to fit in

Proposed architecture

Libraries we used

  • Alamofire: Library for handling HTTP request/response
  • Siesta: Build RESTful requests similar to Retrofit and Restangular
  • SwiftyJSON: A JSON library with exellent error handling

Alamofire + Siesta

Use built-in extension to configure Siesta

let conf = URLSessionConfiguration.default
conf.timeoutIntervalForRequest = 60
conf.timeoutIntervalForResource = 60
self.service = Service(baseURL: "https://my.api.com/api/v1",
  useDefaultTransformers: true,
  networking: AlamofireProvider(configuration: conf))

Parse JSON with SwiftyJSON

self.onCompletion { responseInfo in
  switch responseInfo.response {
  case .success(let entity):
    let json: SwiftyJSON.JSON
    if let _json = entity.content as? SwiftyJSON.JSON {
      json = _json
    } else {
      json = SwiftyJSON.JSON(entity.content)
    }
    var ent = Siesta.Entity<SwiftyJSON.JSON>(content: json,
      contentType: entity.contentType)
    ent.headers = entity.headers
    ent.charset = entity.charset
    ent.timestamp = entity.timestamp
    o.onNext(ent)
  case .failure(let error):
    o.onError(self.parseLadderError(error) ?? error)
  }
  o.onCompleted()
}

Use SwiftyJSON as request body for Siesta

extension Resource {
  func swiftyJSONRequest(_ method: RequestMethod = .get,
    data: JSON? = nil) -> Request {
    if method != .get, let _data = data {
      return self.request(method, text: _data.stringify,
        contentType: "application/json")
    } else {
      return self.request(.get)
    }
  }
}

Object Mapper: map JSON to entity class

First attempt: use Mirror API

By utilizing Swift Mirror API, we can get object information in run-time:

let r = Mirror(reflecting: entity)
print(r.subjectType)
for child in r.children {
  if let label = child.label {
    print("key:  ", label)
  }
  print("value:", child.value)
}
struct User {
  var id: UInt64 = 0
  var name: String = ""
}

Limitaion

  • Mirror API provides reflection mechanism, not dynamic object inspection like NSKeyValueCoding
  • Thus, we can get entity properties dynamically, but cannot set values
  • Swift mapping libraries eventually requires own mapping implementation to work, and the libraries try to make developers write as less codes as possible

Naïve approach: Protocol

protocol SwiftyJSONModel {
  init(from json: JSON)
  mutating func from(_ json: JSON)
}
 
struct User: SwiftyJSONModel {
  init(from json: JSON) {
    self.from(json)
  }
  mutating func from(_ json: JSON) {
    self.id = json["id"].uInt64Value
    self.name = json["name"].stringValue
  }
}

Usage example, combined with generic type class to ensure flexibility

class ResponseTransformer<T: SwiftyJSONModel> {
  static func transform(_ json: JSON) -> T {
    return T.self.init(from: json)
  }
}
 
let user = ResponseTransformer<User>.transform(json)

From entity to JSON?

Two ways to implement this feature:

  1. Manual way: definefunc to() -> JSONin SwiftyJSONModel protocol
  2. Auto way: use Mirror API to inspect key/value pairs (like Wrap)

Pros and cons for this approach

Pros

  • Compose with any libraries you like
  • Add custom features as your need, e.g.
    • Inspect API-specific structures in mapper function
    • Use encode/decode implementations forUserDefaultsdirectly

Cons

  • Heavily couples with JSON library
    • Solution: abstract the JSON library (like Unbox)
  • Re-invent wheels?

Do we really benefit from writing our own solution?

  • Not really, if you are developing apps without lots of time
  • If you are building an SDK for your product, this is a great approach!
    • Low coupling to external dependencies
    • Fine-grained control over your data
    • Façade is good

Is it possible to go further?

Thanks for listening!

Feedbacks are welcome: pics.ee/teaualune