At Airstrip, we have lots of code built up around handling JSON model responses. We have a generic transformation process that can take JSON and instantiate concrete model objects. One of the major difficulties, though, is handling array relationships. Imagine the following model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Our app would receive the following JSON for a call to a specific endpoint that we know describes a
1 2 3 4 5 6 7 8
When we parse this JSON and try to produce a
Person model, we can easily fill in the
name property. The Objective-C property is declared to be an NSString, and the JSON data contains the string “Dustin”. So we can just set the person’s concrete
name property to “Dustin”. We know that the
address property aligns with the
address JSON attribute, so we can construct an
Address instance and create it with this JSON:
Just like we could set the Person’s
name to “Dustin”, we can set this new
number to be “123”, its
street to be “Main St”, and its
zip to be “99999”.
Getting back to parsing the JSON for the
Person, it’s tough when we evaluate the
phoneNumbers property. What we want to happen is to construct two
TelephoneNumber classes, create an array that points to them, and then assign that to the concrete
phoneNumbers property. But our JSON transformation system looks at the
phoneNumbers property and sees the type
We need to tell the transformation system extra information about what belongs inside the NSArray. We need an annotated NSArray.
The unfortunate reality is that it’s just not possible (if it is possible, I would love to be wrong, so tweet me). I’ll describe a bunch of alternatives to try and solve this problem, and give you my suggestions.
The unfortunate IBOutletCollection
I’ll start off with a non-solution. You may be aware of the
IBOutletCollection macro, which you can use to annotate a property:
In Interface Builder you can then hook up multiple label outlets to the
If you were hoping that this could magically be added to our model and used by our transformation system:
You would be sad to look up the definition of IBOutletCollection:
Yeah, it gets removed and does us no good.
Objective C Generics and JSONModel both take an interesting approach. They use protocols to annotate the contents of an NSArray rather than to describe behavior of the NSArray itself. Consider this example:
1 2 3
You then need to create a protocol with the same name as your class:
This absolutely works, and it’s a really clever workaround, but it’s very misleading. For this problem, where we are more concerned with annotation (like JSONModel), and less concerned with strict generics (like Objective C Generics), the syntax implies a lot more than we really want. We aren’t adding strict generics, but it looks like we are. You also have to create a protocol for each of your model classes, which is where the abstraction shows its leaks. I can’t entirely rule this option out because it does function properly. I’m just hesitant of what this might do for a large code base.
Getting your server involved
We could stop trying to annotate our Objective-C code, and annotate the JSON instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
By pushing the burden to the server, we’ve now worked around Objective-C’s limitation and tightly coupled the server with the client’s class names. If your Android and iOS clients both use the same JSON endpoints, then now you have to ignore the
_model field in Android, converge your Android and iOS class names, or add another JSON field:
If you rev your client, you may need to rev your server. Can your new server communicate with old clients? To be fair, there’s already tight coupling between server JSON and client concrete models. Maybe this method is not concerning to you. Maybe it’s a real issue.
I went exploring down this path a bit. Basically:
1 2 3 4 5 6
This looks somewhat appealing. Just subclass NSArray and create your own typed array class. Sure, now every model class needs to create its own array, but even more concerning is how difficult subclassing NSArray is. It does not do what you think it does. Subclassed NSArrays do not provide the same implementation as
[NSArray array]. You have to go implement your own array storage and conform to the NSArray interface. This is something I didn’t want to mess around with at the time, and my investigations ended there. Perhaps you can make this work. If you have, get in touch.
The oh-god-no option
I showed this to my boss, and he said he wanted to throw up. It’s truly terrible, and don’t use it, but I like the hack.
Objective-C limits exactly what can be associated with a property. This guide describes it in detail, but a property has a name and a set of attributes (readonly/readwrite, atomicity, memory management, raw type, and getter/setters). Most of those are unusable booleans or enums, but getter/setter are strings. Rather than using the getter/setter for its normal use, we can stuff annotations in there.
At runtime we can inspect them via
property_getAttributes() and then treat the getter as the NSArray’s annotated type rather than a getter.
I repeat, don’t do this.
ObjectiveCAnnotate is a code generation tool that lets you add source-level annotations to your code that will be translated pre-compile-time.
Will be transformed into:
ObjectiveCAnnotate doesn’t have built-in support for what we want, annotated NSArrays, but you could easily add support for it with their custom annotations.
I wasn’t huge on having to run a pre-compilation step in our build just to get this feature. And adding something like this certainly increases the complexity of your code, making a new-hire’s life more complicated.
Implicit property names
This is actually a reasonable solution, which requires no special annotation. Just name your properties as the plural name of a class. The transformation system can get the property name, singularize it, optionally prepend a namespace prefix, and there’s your annotated NSArray’s type.
In our example where the model class was named
TelephoneNumber, we could change from:
We could have instead changed our model class to be named
PhoneNumber to match the property.
For most code you might not even need to rename. You likely are already naming your properties the same as your classes.
This is not a perfect solution. We are forming a tight coupling between our server and client because the server property names need to match the concrete class names.
It’s also reasonable to have a situation where you have to have different names:
1 2 3 4
NationalOfficial will have an array of private phone numbers and a separate array of secure phone numbers. If we require the property name to be the same as the class name, they can’t both point to
TelephoneNumber, so we would only be able to have one telephone number array.
Explicit property transformer
Finally, we can give up our lofty goal of implicitly deducing this information and rely exclusively on explicitly listing property types.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Each model object needs to override the
-transformableProperties method and return a dictionary of property names to types. The
PropertyType would be a class which can represent primitive types, class types, and annotated collection types.
The obvious downside is that we are having to repeat ourselves in our interface declaration and implementation. But we do get very explicit behavior that can be read by everyone of your developers (new or old), and it makes sense.
My personal view on this is to write as little code as possible. That’s why I set out on this whole process. Our current solution was the explicit property transformer, and I thought it was verbose, redundant, and difficult to maintain.
I think the right balance is in a mix of implicit and explicit. For most scenarios the implicit property names method works great. Once you understand it, it’s clean and beautiful. And if you ever need to support situations where your property names cannot match, use
-transformableProperties. Any property names returned from there are used as specified and all others will be inferred.
Thanks for reading, and I hope this recap gives you enough information to make an informed decision. Please feel free to get in touch, especially if you have a solution I’m missing.