Objective-C and parsing the unknown

So there’s this pattern I encounter every now and then. There is a service that returns some json data. The json data has some polymorphic behavior built-in. What I mean by that is that there is a root node that describes the behavior of the contained child node, which could be different for every child. To provide an example, lets say we are building a sort of a design system where the components are going to be provided by the server. The client then needs to parse the components and render them on screen.

So there could be an Image component which looks like:

{
    "type": "Image",
    "props": {
        "image": "https://via.placeholder.com/300/09f/fff.png",
        "width": 300,
        "height": 300
    }
}

Similarly there could be Text component which looks like:

{
    "type": "Text",
    "props": {
        "text": "This is a text",
        "font-size": 12
    }
}

And many more components to come.

Swift solution

With Swift, the solution I’ve seen implemented in most of the places is to use a giant switch somewhere most like with cyclomatic complexity disabled for the linter. Something like this:

typealias JSON = [String: Any]

func parse(json: JSON) -> Component? {
  guard
    let type = json["type"] as? String,
    let props = json["props"] as? JSON else {
    return nil
  }

  switch type {
  case "Image": return ImageComponent.parse(props: props)
  case "Text": return TextComponent.parse(props: props)
  default: return nil
  }
}

We can try making this less error prone by hiding raw String behind an enum like:

guard 
  let type = (json["type"] as? String).flatMap(ComponentType.init),
  let props = json["props"] as? JSON else {
  return nil
}
switch type {
  case .image: return ImageComponent.parse(props: props)
  case .text: return TextComponent.parse(props: props)
  default: return nil
}

But still there’s no easy way to get rid of that switch statement.

The problem

What’s wrong with that switch statement anyways?

Turns out everything. Behind the scene a switch statement is just an if-else like conditional branching with some syntactic sugar. This sort of type based conditional branching is against everything Object Oriented Programming advocates.

One of the core ideas of OOP is to promote polymorphism over switch statements. In fact, most of the text books start out by an example where there’s a giant switch statement somewhere and clean it up by writing an abstract implementation that is then implemented by every type in its own ways.

You can find many interesting articles that describe this in great details.

Objective-C solution

With Objective-C this is an easy fix because we can parse the type and construct the class name at runtime. And then send the parse message to it.

- (id)parseComponentJSON:(NSDictionary *)componentDict
{
  NSString *type = [componentDict objectForKey:@"type"];
  NSString *componentName = [type stringByAppendingString:@"Component"];
  Class componentClass = NSClassFromString(componentName);
  if (componentClass == nil) {
    NSLog(@"%@ not found", componentName);
    return nil;
  }

  id component = [[componentClass alloc] init];
  if (![component respondsToSelector:@selector(parseJSON:)]) {
    return nil;
  }
  [component performSelector:@selector(parseJSON:)
                  withObject:[componentDict objectForKey:@"props"]];
  return component;
}

The components then can be implement as:

@interface ImageComponent : NSObject
@property (nonatomic, strong) NSURL* imageURL;
@property (nonatomic, assign) CGSize size;

- (void)parseJSON:(NSDictionary *)json;
@end

@implementation ImageComponent
- (void)parseJSON:(NSDictionary *)json;
{
  self.imageURL = [NSURL URLWithString:[json objectForKey:@"image"]];
  self.size = CGSizeMake([[json objectForKey:@"width"] doubleValue],
                         [[json objectForKey:@"height"] doubleValue]);
}

- (NSString *)description
{
  return [NSString stringWithFormat:@"%@, %@",
          self.imageURL,
          NSStringFromCGSize(self.size)];
}
@end

And another unrelated component

@interface TextComponent : NSObject
@property (nonatomic, strong) NSString* text;
@property (nonatomic, assign) double fontSize;

- (void)parseJSON:(NSDictionary *)json;
@end

@implementation TextComponent
- (void)parseJSON:(NSDictionary *)json
{
  self.text = [json objectForKey:@"text"];
  self.fontSize = [[json objectForKey:@"font-size"] doubleValue];
}

- (NSString *)description
{
  return [NSString stringWithFormat:@"%@, %.2f", self.text, self.fontSize];
}
@end

The beauty of this solution is that there is no giant switch statement anywhere. Whenever a new Component is introduced we only have to add another class that knows how to decode the props of that component.

If we want to make this more protocol oriented, we can have a proper Component type that provides an interface for how a Component should look like.

@interface Component : NSObject
- (void)parseJSON:(NSDictionary *)json;
@end

@implementation Component
- (void)parseJSON:(NSDictionary *)json;
{
  [NSException raise:@"AbstractMethod" format:@"Not implemented"];
}
@end

And if we’re that far, we can even make the signature is bit more clean to avoid having 2 steps; One for [[class alloc] init] and other for [instance parseJSON: json]

@interface Component : NSObject
+ (instancetype)componentWithJSON:(NSDictionary *)json;
@end

@implementation Component
+ (instancetype)componentWithJSON:(NSDictionary *)json;
{
  [NSException raise:@"AbstractMethod" format:@"Not implemented"];
  return nil;
}
@end

With that our parser simply looks like:

- (Component *)parseComponentJSON:(NSDictionary *)componentDict
{
  NSString *type = [componentDict objectForKey:@"type"];
  NSString *componentName = [type stringByAppendingString:@"Component"];
  Class componentClass = NSClassFromString(componentName);
  return [componentClass componentWithJSON:[componentDict objectForKey:@"props"]];
}

Now if you notice we’re not checking of any nil. That is because sending messages to nil is safe and very well documented behavior.

If the method returns an object, then a message sent to nil returns 0 (nil).

This means that we don’t have to check for intermediate nil if we our method is designed to return nil if parsing fails. So, we can even clean up further by removing all temporary variables in our parser

- (Component *)parseComponentJSON:(NSDictionary *)componentDict
{
  NSString *componentName = [[componentDict objectForKey:@"type"] stringByAppendingString:@"Component"];
  return [NSClassFromString(componentName) componentWithJSON:[componentDict objectForKey:@"props"]];
}

This is entire code:

@interface Component : NSObject
+ (instancetype)componentWithJSON:(NSDictionary *)json;
@end

@implementation Component
+ (instancetype)componentWithJSON:(NSDictionary *)json;
{
  [NSException raise:@"AbstractMethod" format:@"Not implemented"];
  return nil;
}
@end

@interface ImageComponent : Component
@property (nonatomic, strong) NSURL* imageURL;
@property (nonatomic, assign) CGSize size;

+ (instancetype)componentWithJSON:(NSDictionary *)json;
@end

@implementation ImageComponent
+ (instancetype)componentWithJSON:(NSDictionary *)json;
{
  ImageComponent *component = [[ImageComponent alloc] init];
  component.imageURL = [NSURL URLWithString:[json objectForKey:@"image"]];
  component.size = CGSizeMake([[json objectForKey:@"width"] doubleValue],
                         [[json objectForKey:@"height"] doubleValue]);
  return component;
}

- (NSString *)description
{
  return [NSString stringWithFormat:@"%@, %@",
          self.imageURL,
          NSStringFromCGSize(self.size)];
}
@end

@interface TextComponent : Component
@property (nonatomic, strong) NSString* text;
@property (nonatomic, assign) double fontSize;

+ (instancetype)componentWithJSON:(NSDictionary *)json;
@end

@implementation TextComponent
+ (instancetype)componentWithJSON:(NSDictionary *)json;
{
  TextComponent *component = [[TextComponent alloc] init];
  component.text = [json objectForKey:@"text"];
  component.fontSize = [[json objectForKey:@"font-size"] doubleValue];
  return component;
}

- (NSString *)description
{
  return [NSString stringWithFormat:@"%@, %.2f", self.text, self.fontSize];
}
@end

@interface ComponentParser : NSObject
- (NSArray *)parseComponentsWithData:(NSData *)data;
@end

@implementation ComponentParser

- (Component *)parseComponentJSON:(NSDictionary *)componentDict
{
  NSString *componentName = [[componentDict objectForKey:@"type"] stringByAppendingString:@"Component"];
  return [NSClassFromString(componentName) componentWithJSON:[componentDict objectForKey:@"props"]];
}

- (NSArray *)parseComponentsWithData:(NSData *)jsonData
{
  NSError *error = nil;
  NSDictionary *componentDict = [NSJSONSerialization JSONObjectWithData:jsonData
                                                            options:NSJSONReadingFragmentsAllowed
                                                              error:&error];
  NSAssert(error == nil, @"Parsing failure %@", [error localizedDescription]);

  NSArray *componentJSONs = [componentDict objectForKey:@"components"];
  NSMutableArray *components = [NSMutableArray arrayWithCapacity:[componentJSONs count]];
  for (NSDictionary *componentJSON in componentJSONs) {
    Component *component = [self parseComponentJSON:componentJSON];
    if (component != nil) {
      [components addObject:component];
    } else {
      NSLog(@"Unable to parse %@", componentJSON);
    }
  }
  return components;
}

@end

@interface Test : NSObject
- (void)run;
@end

@implementation Test

- (void)run
{
  NSError *error = nil;
  NSURL *path = [[NSBundle mainBundle] URLForResource:@"components" withExtension:@"json"];
  NSAssert(path != nil, @"No path");

  NSData *jsonData = [NSData dataWithContentsOfURL:path options:NSDataReadingMappedIfSafe error:&error];
  NSAssert(error == nil, @"File read failed %@ | %@", path, [error localizedDescription]);

  ComponentParser *parser = [[ComponentParser alloc] init];
  NSArray *components = [parser parseComponentsWithData:jsonData];

  NSLog(@"total components: %ld", [components count]);

  for (Component *component in components) {
    NSLog(@"%@", [component description]);
  }
}

@end

And a sample output with one component not implemented

Unable to parse {
    props =     {
        link = "https://whackylabs.com";
    };
    type = Web;
}
total components: 2
https://via.placeholder.com/300/09f/fff.png, {300, 300}
This is a text, 12.00

The more I think about it, I think very few languages provide a clean way to express OOP patterns. And in this case Objective-C seems to be cleaner implementation than Swift.

If you know of a better way to solve this problem in Swift like languages, please let me know on twitter

References

  1. Object-Oriented Programming in Objective-C – youtube.com
  2. Cocoa Design Patterns – developer.apple.com