MLObject 1.0.1

MLObject 1.0.1

TestsTested βœ—
LangLanguage SwiftSwift
License MIT
ReleasedLast Release May 2018
SPMSupports SPM βœ—

Maintained by Niels Andriesse.



MLObject 1.0.1

  • By
  • Zilly, Inc.

Version License Platform

To install Milk, just add the following line to your Podfile:

pod "MLObject"

What is Milk? πŸ₯›

Here at Zilly, we use Realm. Even though it has its caveats (Threading, Notifications), it’s pretty great in many respects. You get easy persistence of your model to disk, a comprehensive observer system, powerful querying, and lots more.

We also use JSON for client-server communication, which we really wanted to parse automatically to actual object instances, because writing a parser for every model separately is a. ) very time consuming, b. ) a looooot of unnecessary boilerplate and c. ) asking for bugs.

Realm has some built-in support for this, but it wasn't really enough for what we wanted to do.

Not a problem, right? There are lots of frameworks out there like JSONModel that automatically parse JSON to model objects. Well, yeah, but not if you still use Objective-C for a significant part of your app (for legacy reasons), and both Realm and these JSON parsing frameworks require your objects to inherit from their base classes.

What to do, what to do? Well, we decided the answer to that question was to write our own framework that parses JSON directly to Realm objects. We call it Milk :) .

Let's walk through what it can do:

A Basic Example

In the simplest case, just subclass MLObject and add the properties you need:

@import MLObject;

@interface Cat : MLObject

@property NSString *catID;
@property NSString *preferredName;

@end

You can now already parse Cat objects from JSON by doing [Cat importedFromJSON:] (or [Cat convertedFromJSON:] if you just want to parse the object without also importing it into Realm).

Milk automatically maps snake_case JSON keys to lowerCamelCase object names as needed. This also works if the property name contains an acronym (e.g. catID in the example above). So for the example above both:

{
  cat_id: "19378372",
  preferred_name: "Jellybean"
}

and

{
  catID: "19378372",
  preferredName: "Jellybean"
}

work.

Performance with Many Objects

It's pretty common to want to parse multiple objects in one go. With Milk, you can do this efficiently (i.e. in a single Realm write transaction) by doing [Cat importArrayOfObjectsFromJSON:].

What If the JSON Structure Doesn't Match Your Model Structure?

The server doesn't always give you JSON in the structure that you want (i.e. matching your model). Consider the following example:

{
  cat_id: "19378372",
  name: "Jellybean",
  meta: {
    coordinates: {
      lon: 4.886
    }
  },
  last_known_latitude: 52.374
}

And the below model structure:

@import MLObject;

@interface Location : MLObject

@property NSNumber<RLMDouble> *latitude;
@property NSNumber<RLMDouble> *longitude;

@end

@interface Cat : MLObject

@property NSString *catID;
@property NSString *name;
@property Location *location;

@end

As you can see, the JSON structure is quite different from the model structure, so our Cat would normally fail to parse in this case. However, solving this problem is easy with Milk. Just implement Cat's fromJSONMapping as follows:

@implementation Cat

+ (NSDictionary<NSString *, NSString *> *)fromJSONMapping {
  return @{
    @"location" : @"meta.coordinates",
    @"location.latitude" : @"last_known_latitude"
  };
}

@end

Take a second to look at what's going on here. We're reaching down into the JSON (using a key path) to get a partial Location object from meta.coordinates, which we're then completing (again using a key path, but this time to specify which property to assign to) with last_known_latitude.

The ability to map any JSON structure to any model structure like this means that the way you set up your models client-side can be completely independent from the way models are set up in the back-end. Pretty cool, right?

Note 1

The Location object also needs to implement fromJSONMapping, to account for the mismatch between the lon JSON key found under meta.coordinates and the longitude property name:

@implementation Location

+ (NSDictionary<NSString *, NSString *> *)fromJSONMapping {
  return @{
    @"latitude" : @"lat",
    @"longitude" : @"lon"
  };
}

@end
Note 2

If the JSON under meta.coordinates would have already contained a complete Location object, the value for the latitude property specified there would have been overridden by the value found under last_known_latitude.

Converting Back to JSON

To convert back to JSON, just call toJSON on your object. This will return a Dictionary with the object's property names in snake_case mapped to their respective values converted to JSON (i.e. toJSON is called on each value).

You can override the default JSON key for a property using toJSONMapping. This again supports key paths. So you could for example convert a Cat object to the following JSON:

{
  cat_id: "19378372",
  name_data: {
    preferred: "Jellybean"
  }
}

by implementing Cats toJSONMapping as follows:

@implementation Cat

+ (NSDictionary<NSString *, NSString *> *)toJSONMapping {
  return @{
    @"preferredName" : @"name_data.preferred"
  };
}

@end

A Note on Warnings

You probably want to know if something failed to parse, and why. This may be obvious in simple cases, but when you're importing a large number of objects it's easy to miss when one of them gets lost in parsing. To help with this, Milk prints a warning if an object fails to parse, indicating the JSON it was trying to parse and the type it was trying to parse it to.