Skip to content

Instantly share code, notes, and snippets.

@tapi
Created April 25, 2012 11:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tapi/2489095 to your computer and use it in GitHub Desktop.
Save tapi/2489095 to your computer and use it in GitHub Desktop.
Dictionary <--> Object Mapper for Objective-C
//
// NSObject+SMDictionaryMapping.h
// SoundTrack
//
// Created by Paddy O'Brien on 12-04-24.
// Copyright (c) 2012 Paddy O'Brien. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol SMMappedObject <NSObject>
+ (NSDictionary *)mappedKeys;
@end
@interface NSObject (SMDictionaryMapping)
- (id)initWithDictionary:(NSDictionary *)dictionary;
- (void)updateWithDictionary:(NSDictionary *)dictionary;
@end
//
// NSObject+DictionaryMapping.m
// SoundTrack
//
// Created by Paddy O'Brien on 12-04-24.
// Copyright (c) 2012 Paddy O'Brien. All rights reserved.
//
#import "NSObject+DictionaryMapping.h"
@interface NSObject ()
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary;
@end
@implementation NSObject (DictionaryMapping)
- (id)initWithDictionary:(NSDictionary *)dictionary
{
if (self) {
[self mapDictionaryToProperties:dictionary];
}
return self;
}
- (void)updatePropertiesWithDictionary:(NSDictionary *)dictionary
{
[self mapDictionaryToProperties:dictionary];
}
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary
{
NSArray* keys = [dictionary allKeys];
for (NSString *key in keys) {
NSString *mappedKey = key;
if ([self conformsToProtocol:@protocol(SMMappedObject)] && [self respondsToSelector:@selector(mappedKeys)]) {
NSDictionary *mappedKeys = [self performSelector:@selector(mappedKeys)];
if ([mappedKeys valueForKey:key]) {
mappedKey = [mappedKeys valueForKey:key];
}
}
[self setValue:[dictionary valueForKey:key] forKey:mappedKey];
}
}
@end
//
// NSObject+SMDictionaryMapping.h
//
// Created by Paddy O'Brien on 12-04-24.
// Copyright (c) 2012 Paddy O'Brien. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol SMMappedObject <NSObject>
+ (NSDictionary *)mappedKeys;
@end
@interface NSObject (SMDictionaryMapping)
- (id)initWithDictionary:(NSDictionary *)dictionary;
- (void)updateWithDictionary:(NSDictionary *)dictionary;
@end
//
// NSObject+DictionaryMapping.m
//
// Created by Paddy O'Brien on 12-04-24.
// Copyright (c) 2012 Paddy O'Brien. All rights reserved.
//
#import "NSObject+DictionaryMapping.h"
@interface NSObject ()
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary;
@end
@implementation NSObject (DictionaryMapping)
- (id)initWithDictionary:(NSDictionary *)dictionary
{
if (self) {
[self mapDictionaryToProperties:dictionary];
}
return self;
}
- (void)updatePropertiesWithDictionary:(NSDictionary *)dictionary
{
[self mapDictionaryToProperties:dictionary];
}
- (void)mapDictionaryToProperties:(NSDictionary *)dictionary
{
NSArray* keys = [dictionary allKeys];
for (NSString *key in keys) {
NSString *mappedKey = key;
if ([self conformsToProtocol:@protocol(SMMappedObject)] && [self respondsToSelector:@selector(mappedKeys)]) {
NSDictionary *mappedKeys = [self performSelector:@selector(mappedKeys)];
if ([mappedKeys valueForKey:key]) {
mappedKey = [mappedKeys valueForKey:key];
}
}
id value = ([[dictionary valueForKey:key] isKindOfClass:[NSNull class]]) ? nil : [dictionary valueForKey:key];
[self setValue:value forKey:mappedKey];
}
}
@end
@tapi
Copy link
Author

tapi commented Apr 25, 2012

Not tested, just a proof of concept.

@tapi
Copy link
Author

tapi commented Apr 25, 2012

Note: the problem this is designed to solve is initializing an object from JSON deserialized to an NSDictionary.
the whole mapped key nonsense is to handle cases where JSON propertied conflict with reserved words such as id.

@ashfurrow
Copy link

Neat idea - this is something I've struggled with, too. It would also be cool to have something where NSNull isn't (necessarily) successfully set as a property, since the dynamic runtime won't complain at the time, but sending the length selector to NSNull crashes your app later.

@tapi
Copy link
Author

tapi commented Apr 25, 2012

Something like this?

id value = ([[dictionary valueForKey:key] isKindOfClass:[NSNull class]]) ? nil : [dictionary valueForKey:key];
[self setValue:value forKey:mappedKey];

@ashfurrow
Copy link

Yeah, I mean, NSNull is supposed to be a sentinel, but using it kind of sucks (imo). I like this solution, since for all cases but an NSMutableDictionary, nil is going to be fine. Are you planning to implement setValue:forUndefinedKey:?

@tapi
Copy link
Author

tapi commented Apr 25, 2012

Im of two minds. On one hand Id like the code to run even in the event of errors. On the other hand if there's an error in data modelling between classes and JSON responses id like it to fail fast so its more likely to be caught during testing.

It's not a whole lot different than implementing the method once per class and doing the mapping manually except now it'll crash if you send it the wrong dictionary instead of just giving you and empty or incorrectly populated object.

@tapi
Copy link
Author

tapi commented Apr 25, 2012

Frig I just realized that this wont handle nested objects.

@ashfurrow
Copy link

Could use reflection in setValue:forKey: or the property setter.

@warwick
Copy link

warwick commented Apr 25, 2012

Could you write up a quick snippet that uses this? I think I see the general idea, but I'm having trouble stepping through the code. I'll admit, that might be because I just woke up.

@ashfurrow
Copy link

Well, your current approach would work except if you're being passed in a sub-entity (like the user model of a photo model). So you could do something like this:

-(void)setUserModel:(id)theUserModel
{
    if ([theUserModel isKindOfClass:[NSDictionary class]])
    {
        self.userModel = [[UserModel alloc] initWithDictionary:theUserModel];
        return;
    }

    _userModel = theUserModel;
}

Thoughts?

@tapi
Copy link
Author

tapi commented Apr 26, 2012

Yes but what if you actually want a dictionary as a property on an object? Interogate the class of the property to see if it conforms to the protocol perhaps?

@warwick an example might look like this

@interface Contact : NSObject <SMMappedObject>
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) Email *email;
@end

Later we want to get a bunch of contacts from some webservice that spits back JSON

- (NSArray *)getMyContacts
{
    NSData *JSONData = [MagicWebRequestThatRequiresNoSetup GetURL:@"www.myaddressbook.com/contacts"];
    NSArray *contacts = [JSONData parseObjectFromJSON];

    NSMutableArray* retval =  [NSMutableArray array];
    for (NSDictionary *contactData in contact) {
        Contact *newContact = [[Contact alloc] initWithDictionary:contactData];
        [retval addObject:newContact];
    }

    return retval;
}

The idea is to have you local domain object populated without having to slog through writing your own mappings.

@aaronpeterson
Copy link

I'm curious about sub-entities and arrays of sub-entities, too. Would it be best to define those in the model class as an array of strings (JSON key names) you want explicitly mapped to sub-entities? One thing all of my JSON keys have in common is that the sub-entity keys are all upper-case first character. This could be the "auto" mode; if a mapped key is upper-case first character it looks for a model class of same name and maps accordingly. I haven't looked to see what something like RestKit does. Just seems like something that could be a single NSObject category rather than a giant library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment