93

I have a core data object graph (consisting of two entities linked by a to-many relationship).

I was curious, as a relatively inexperienced iPhone developer, whether anyone could recommend an approach, and a suitable JSON implementation for the iPhone, which would allow me to:

  1. convert the core data records into a JSON string (whilst maintaining the relationship between the entities); and

  2. convert the JSON string back into core data objects (again preserving the relationship between entities).

I have searched, unsuccessfully, for a tutorial/code sample on this point so any assistance would be gratefully received.

pkamb
  • 26,648
  • 20
  • 124
  • 157
Urizen
  • 2,213
  • 6
  • 23
  • 32
  • 3
    For anyone looking into this for iOS5, there's now `NSJSONSerialization` http://developer.apple.com/library/mac/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html http://stackoverflow.com/questions/6726899/nsjsonserialization-in-ios5 – nicerobot Dec 03 '11 at 04:05
  • I know that this question is a little bit old, but I have created a simple library called [OSReflectionKit](https://github.com/iAOS/OSReflectionKit), which allows you to serialize/deserialize objects to/from JSON, using [NSJSONSerialization](http://developer.apple.com/library/ios/#documentation/Foundation/Reference/NSJSONSerialization_Class/Reference/Reference.html), or NSDictionary. It also supports Core Data objects. – Alexandre OS Aug 06 '13 at 14:27

8 Answers8

103

First, pick a JSON library to use, I personally like TouchJSON but several others out there are quite nice as well. The complicated part, although not very hard, is to convert your managed objects into suitable structures for the conversion. I wrote this real quick so it may have an error or two :)

The methods you call are:

- (NSString*)jsonStructureFromManagedObjects:(NSArray*)managedObjects;
- (NSArray*)managedObjectsFromJSONStructure:(NSString*)json withManagedObjectContext:(NSManagedObjectContext*)moc;

And the implementation is as follows:

- (NSDictionary*)dataStructureFromManagedObject:(NSManagedObject*)managedObject
{
  NSDictionary *attributesByName = [[managedObject entity] attributesByName];
  NSDictionary *relationshipsByName = [[managedObject entity] relationshipsByName];
  NSMutableDictionary *valuesDictionary = [[managedObject dictionaryWithValuesForKeys:[attributesByName allKeys]] mutableCopy];
  [valuesDictionary setObject:[[managedObject entity] name] forKey:@"ManagedObjectName"];
  for (NSString *relationshipName in [relationshipsByName allKeys]) {
    NSRelationshipDescription *description = [[[managedObject entity] relationshipsByName] objectForKey:relationshipName];
    if (![description isToMany]) {
      NSManagedObject *relationshipObject = [managedObject valueForKey:relationshipName];
      [valuesDictionary setObject:[self dataStructureForManagedObject:relationshipObject] forKey:relationshipName];
      continue;
    }
    NSSet *relationshipObjects = [managedObject objectForKey:relationshipName];
    NSMutableArray *relationshipArray = [[NSMutableArray alloc] init];
    for (NSManagedObject *relationshipObject in relationshipObjects) {
      [relationshipArray addObject:[self dataStructureForManagedObject:relationshipObject]];
    }
    [valuesDictionary setObject:relationshipArray forKey:relationshipName];
  }
  return [valuesDictionary autorelease];
}

- (NSArray*)dataStructuresFromManagedObjects:(NSArray*)managedObjects
{
  NSMutableArray *dataArray = [[NSMutableArray alloc] init];
  for (NSManagedObject *managedObject in managedObjects) {
    [dataArray addObject:[self dataStructureForManagedObject:managedObject]];
  }
  return [dataArray autorelease];
}

- (NSString*)jsonStructureFromManagedObjects:(NSArray*)managedObjects
{
  NSArray *objectsArray = [self dataStructuresFromManagedObjects:managedObjects];
  NSString *jsonString = [[CJSONSerializer serializer] serializeArray:objectsArray];
  return jsonString;
}

- (NSManagedObject*)managedObjectFromStructure:(NSDictionary*)structureDictionary withManagedObjectContext:(NSManagedObjectContext*)moc
{
  NSString *objectName = [structureDictionary objectForKey:@"ManagedObjectName"];
  NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:objectName inManagedObjectContext:moc];
  [managedObject setValuesForKeysWithDictionary:structureDictionary];

  for (NSString *relationshipName in [[[managedObject entity] relationshipsByName] allKeys]) {
    NSRelationshipDescription *description = [relationshipsByName objectForKey:relationshipName];
    if (![description isToMany]) {
      NSDictionary *childStructureDictionary = [structureDictionary objectForKey:relationshipName];
      NSManagedObject *childObject = [self managedObjectFromStructure:childStructureDictionary withManagedObjectContext:moc];
      [managedObject setObject:childObject forKey:relationshipName];
      continue;
    }
    NSMutableSet *relationshipSet = [managedObject mutableSetForKey:relationshipName];
    NSArray *relationshipArray = [structureDictionary objectForKey:relationshipName];
    for (NSDictionary *childStructureDictionary in relationshipArray) {
      NSManagedObject *childObject = [self managedObjectFromStructure:childStructureDictionary withManagedObjectContext:moc];
      [relationshipSet addObject:childObject];
    }
  }
  return managedObject;
}

- (NSArray*)managedObjectsFromJSONStructure:(NSString*)json withManagedObjectContext:(NSManagedObjectContext*)moc
{
  NSError *error = nil;
  NSArray *structureArray = [[CJSONDeserializer deserializer] deserializeAsArray:json error:&error];
  NSAssert2(error == nil, @"Failed to deserialize\n%@\n%@", [error localizedDescription], json);
  NSMutableArray *objectArray = [[NSMutableArray alloc] init];
  for (NSDictionary *structureDictionary in structureArray) {
    [objectArray addObject:[self managedObjectFromStructure:structureDictionary withManagedObjectContext:moc]];
  }
  return [objectArray autorelease];
}

Now this is recursive so you can easily end up translating your entire persistent store if you are not careful. Watch your relationships and make sure that they only go "down" the object tree so that you only get the objects you want translated.

pkamb
  • 26,648
  • 20
  • 124
  • 157
Marcus S. Zarra
  • 46,143
  • 9
  • 99
  • 181
  • Thank you again for another excellent answer and for your very helpful book! :) – Urizen Mar 02 '10 at 22:11
  • 2
    Hi Marcus. I've just tried the code above (with some minor amendments to make it compile and the execution seems to go on indefinitely until the app crashes). Sorry to bother you but I was curious if you could perhaps point me in the right direction for solving this problem. It seems to happen with the recursion in the datastructureFromManagedObject method... – Urizen May 04 '10 at 13:17
  • 1
    Depends on your data structure. If your model will produce a loop then it will run forever. Review your data model and either make sure it is a tree design or put logic stops in the recursive code to prevent looping. – Marcus S. Zarra May 04 '10 at 14:35
  • Thanks for your response. I think that I've managed to solve the endless recursion problem but the code now throws an exception which seems to suggest that NSDate attributes cannot be serialised (i.e. Terminating app due to uncaught exception 'NSGenericException', reason: 'Cannot serialize data of type '__NSCFDate'') Is this the case? If so, is there any way around this? – Urizen May 04 '10 at 17:57
  • I've got round the above-mentioned problem by storing the value returned by [date timeIntervalSince1970] as an NSNumber. – Urizen May 04 '10 at 22:19
  • Clean up what? If you are new to Core Data then you should be grasping the basics of KVC/KVO before trying to tackle recursive code like this. There is nothing dirty about this code, it is a clean example. It's major flaw is that it does not handle edge cases; which would make the code *harder* to read. – Marcus S. Zarra Oct 25 '10 at 22:06
  • `for (NSString *relationshipName in [relationshipsByName allKeys])` could be written as `for (NSString *relationshipName in relationshipsByName)` – user102008 Jan 18 '11 at 22:50
  • I am trying to follow this example to convert JSON response to core-data objects but I am stuck with an error:-[__NSCFDictionary entity]: unrecognized selector sent to instance on setValuesForKeysWithDictionary method. Can someone direct me on what am I doing wrong? – Vibhor Goyal Jan 03 '12 at 18:54
  • @VibhorGoyal Set a breakpoint in the methods above and walk through the code one data point at a time and see where the issue is. Probably something is not matching between the JSON and your model or you are converting a string to a number or number to date or something along those lines. – Marcus S. Zarra Jan 03 '12 at 19:01
  • Thanks for your response Marcus, I did set up a break point, and as a said earlier, it crashes on setValuesForKeysWithDictionary. – Vibhor Goyal Jan 03 '12 at 19:08
  • I have double checked that all the names in my model are exactly same as JSON and also I am not doing any weird conversion. – Vibhor Goyal Jan 03 '12 at 19:31
  • @VibhorGoyal What is the key and what is the value? Do they match with what your model is expecting? What is the attribute type in your model for the key it is crashing on? – Marcus S. Zarra Jan 03 '12 at 19:40
  • There are multiple keys and values, I am not able to debug which key its crashing on, is there a way to find out which key it is crashing on? – Vibhor Goyal Jan 03 '12 at 19:50
  • @VibhorGoyal yes change the `-setValuesForKeysWithDictionary:` into a loop that iterates over the keys in the dictionary and calls `-setValue: forKey:` for each key. That will let you see which key is the issue. – Marcus S. Zarra Jan 03 '12 at 19:53
  • If I use -setValue: forKey: , it does not throw any error. I am guessing that its failing due to the relationships. Is there any working example code that I can follow? – Vibhor Goyal Jan 03 '12 at 20:30
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/6365/discussion-between-marcus-s-zarra-and-vibhor-goyal) – Marcus S. Zarra Jan 03 '12 at 23:15
  • I get an "Expected expression" error on the line `[valuesDictionary setValue:[self dataStructureForManagedObject:]];` - I am trying to work out what it's supposed to be, any clues? Sorry if I'm being thick :( – jowie Jun 26 '12 at 10:34
  • 1
    Have you actually tried runnning this code? There are so many errors. dataStructureForManagedObject doesnt even exist. I thought it might just be a typo but if you change it to dataStructureFromManagedObject the whole thing just loops infinitely bouncing back between relationship pairs. Am i missing some extra code here? – Chris Mitchelmore Nov 09 '12 at 13:22
  • 1
    This code example was written in the browser two years ago. It was intended to inspire not be a copy and paste. As for an infinite loop, that means you have a loop in your model and you would then need to add model specific logic to your app to break the cycle. There are several ways to do that that are not in this example. – Marcus S. Zarra Nov 09 '12 at 17:47
  • Any elegant solution to avoid the exception arising when some of the attributes are of type Date? – Jean-Denis Muys Jul 12 '13 at 09:52
  • Several, you could use a KVC override to accept the string and convert the date, you could catch it in the set logic and watch for the attribute on the Core Data side to try and parse, I am sure there are lots of other ways as well. – Marcus S. Zarra Jul 12 '13 at 16:55
12

I just wanted to point out a small typo, that caused the code to crash, and hopefully this will save you a few min.

- (NSArray*)dataStructuresFromManagedObjects:(NSArray*)managedObjects {

    NSMutableArray *dataArray = [[NSArray alloc] init];
    for (NSManagedObject *managedObject in managedObjects) {
        [dataArray addObject:[self dataStructureFromManagedObject:managedObject]];
    }
    return [dataArray autorelease];
}

The NSMutableArray *dataArray = [[NSArray alloc] init]; // This should be NSMutableArray

really should be NSMutableArray *dataArray = [[NSMutableArray alloc] init];

that is all.

thank you

Matteo Alessani
  • 9,864
  • 4
  • 37
  • 56
creativeKoder
  • 299
  • 3
  • 13
10

Synchronizing Core Data with Rails is a detailed presentation that includes sample code for serializing/deserializing your Core Data objects to/from JSON (skip to slide 55 for the Core Data part). His sample code assumes a fairly simple model without relationships, though I think it would be pretty easy to extend.

The presentation also goes into some detail about keeping your Core Data model in sync with a REST-based web application, with pointers to some useful libraries, including ObjectiveResource and ASIHTTPRequest. Not sure if that's what you're trying to do, but it's worth a look even for the Core Data code.

Christopher Pickslay
  • 17,099
  • 6
  • 77
  • 91
7

If you have an NSDate in your managed object, as mentioned above in one of the comments, you'll have problems serializing the object containing the NSDate. A simple fix is to add a JSONDataRepresentation method to NSDate using objective-c categories.

Add these two files to your project:

NSdate.h:

#import <Foundation/Foundation.h>

@interface NSDate (jsondatarepresentation) 

- (NSData*) JSONDataRepresentation;

@end

NSDate.m:

#import "NSDate.h"

@implementation NSDate (jsondatarepresentation)

- (NSData*) JSONDataRepresentation {
    return [[[NSNumber numberWithDouble:[self timeIntervalSince1970]] stringValue] dataUsingEncoding:NSUTF8StringEncoding];
}

@end
Amal T S
  • 3,037
  • 2
  • 22
  • 51
joshaidan
  • 3,722
  • 1
  • 20
  • 17
3

Just thought id post a quick update to this question. I followed the Answers by Marcus and Brandon and came up with this for JSON exporting (it uses TouchJSON still):

- (NSData*)jsonStructureFromManagedObjects:(NSArray*)managedObjects
{
    NSArray *objectsArray = [self dataStructuresFromManagedObjects:managedObjects];
    NSData *jsonData      = [[CJSONSerializer serializer] serializeArray:objectsArray error:nil];
    return jsonData;
}

- (NSArray*)dataStructuresFromManagedObjects:(NSArray*)managedObjects
{
    NSMutableArray *dataArray = [[NSMutableArray alloc] init];
    for (NSManagedObject *managedObject in managedObjects) {
        [dataArray addObject:[self dataStructureFromManagedObject:managedObject]];
    }
    return dataArray;
}

- (NSDictionary*)dataStructureFromManagedObject:(NSManagedObject*)managedObject
{
    NSDictionary *attributesByName        = [[managedObject entity] attributesByName];
    NSDictionary *relationshipsByName     = [[managedObject entity] relationshipsByName];
    NSMutableDictionary *valuesDictionary = [[managedObject dictionaryWithValuesForKeys:[attributesByName allKeys]] mutableCopy];
    [valuesDictionary setObject:[[managedObject entity] name] forKey:@"ManagedObjectName"];

    for (NSString *relationshipName in [relationshipsByName allKeys]) {

        NSRelationshipDescription *description = [[[managedObject entity] relationshipsByName] objectForKey:relationshipName];

        if ([[[description userInfo] objectForKey:@"isExportable"] boolValue] == YES) {

            if (![description isToMany]) {
                NSManagedObject *relationshipObject = [managedObject valueForKey:relationshipName];
                if (relationshipObject) {
                    [valuesDictionary setObject:[self dataStructureFromManagedObject:relationshipObject] forKey:relationshipName];
                }

                continue;
            }

            NSSet *relationshipObjects        = [managedObject valueForKey:relationshipName];
            NSMutableArray *relationshipArray = [[NSMutableArray alloc] init];

            for (NSManagedObject *relationshipObject in relationshipObjects) {
                [relationshipArray addObject:[self dataStructureFromManagedObject:relationshipObject]];
            }

            [valuesDictionary setObject:relationshipArray forKey:relationshipName];

        }

    }
    return valuesDictionary;
}

I couldn't get the import working, maybe that has something to do with the fact that I'm using Magical Record I'm not sure, so Im just looping through the incoming JSON stream and creating objects manually...

Carl Taylor
  • 91
  • 1
  • 9
2

I came across this post which works very well.

http://touchalicious.com/blog/2009/10/25/turn-core-data-models-into-json.html

Since this is recursive, many-to-many relationships are going to keep looping through themselves. To avoid this, I added an "isExportable" key to the user info dictionary of the relationships in my Core Data model. You can then check for this key and choose to not loop through relationships without it.

enter image description here

if ([property isKindOfClass:[NSRelationshipDescription class]])
    {
        NSRelationshipDescription *relationshipDescription = (NSRelationshipDescription *)property;

        if ([[[relationshipDescription userInfo] objectForKey:@"isExportable"] boolValue] == YES)
        {
            NSString *name = [relationshipDescription name];

            if ([relationshipDescription isToMany])
            {
                NSMutableArray *arr = [properties valueForKey:name];
                if (!arr)
                {
                    arr = [[NSMutableArray alloc] init];
                    [properties setValue:arr forKey:name];
                }

                for (NSManagedObject *o in [self mutableSetValueForKey:name])
                {
                    [arr addObject:[o propertiesDictionary]];
                }
            }
            else
            {
                NSManagedObject *o = [self valueForKey:name];
                [properties setValue:[o propertiesDictionary] forKey:name];
            }
        }
    }
}
Brandon Schlenker
  • 5,028
  • 32
  • 58
2

There is a lib that does the JSON synchronization for you : https://github.com/sixdegrees/lidenbrock

1

Marcus S. Zarra has inspired me to bring the recursive idea to a working version. In this version you don't need to set a key in CoreData and you can cut and paste it in your project :-)

// MARK: - encoding and decoding CoreData entity to dictionary

func dataStructureFromManagedObject( managedObject:NSManagedObject?, parentEntity: NSEntityDescription? = nil) -> NSMutableDictionary {
    if (managedObject != nil) {
        var attributesByName: NSDictionary = managedObject!.entity.attributesByName
        var relationshipsByName: NSDictionary  = managedObject!.entity.relationshipsByName
        var valuesImmutableDictionary: NSDictionary = managedObject!.dictionaryWithValuesForKeys( attributesByName.allKeys)
        var valuesDictionary: NSMutableDictionary = valuesImmutableDictionary.mutableCopy() as NSMutableDictionary
        valuesDictionary.setObject( managedObject!.entity.name!, forKey: "ManagedObjectName")
        for relationshipNameObject in relationshipsByName.allKeys {
            var relationshipName: NSString = relationshipNameObject as  NSString
            var relationshipDescription: NSRelationshipDescription? = relationshipsByName.objectForKey( relationshipName) as? NSRelationshipDescription
            if !relationshipDescription!.toMany {
                // ono to one
                if parentEntity == nil || (relationshipDescription! as NSRelationshipDescription).destinationEntity != parentEntity! {
                    // no parent or relationship is "downward" -> object for relationship must be added
                    var relationshipObject: NSManagedObject? = managedObject!.valueForKey( relationshipName) as? NSManagedObject
                    var relationshipObjectDictionary: NSMutableDictionary = self.dataStructureFromManagedObject( relationshipObject, parentEntity: managedObject?.entity)
                    valuesDictionary.setObject( relationshipObjectDictionary, forKey: relationshipName)
                } else {
                    // relationship is "upward" -> nothing to do
                }
            } else {
                // one to many -> all objects must be added
                var relationshipObjects: NSSet = managedObject!.mutableSetValueForKey( relationshipName)
                var relationshipArray:NSMutableArray = []
                for relationshipObjectRaw in relationshipObjects {
                    var relationshipObject:NSManagedObject? = relationshipObjectRaw as? NSManagedObject
                    if relationshipObject != nil && !relationshipObject!.entity.isKindOfEntity( managedObject!.entity) {
                        relationshipArray.addObject(self.dataStructureFromManagedObject( relationshipObject, parentEntity: managedObject?.entity))
                    }
                }
                valuesDictionary.setObject( relationshipArray, forKey: relationshipName)
            }
        }
        return valuesDictionary
    } else {
        return NSMutableDictionary()
    }
}

func managedObjectFromStructure( structureDictionary: NSDictionary, moc: NSManagedObjectContext, parentObject: NSManagedObject? = nil) -> NSManagedObject {
    if structureDictionary.count > 0 {
        var objectName:NSString = structureDictionary.objectForKey( "ManagedObjectName") as NSString
        var managedObject:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName( objectName, inManagedObjectContext: moc) as NSManagedObject
        var relationshipsByName: NSDictionary  = managedObject.entity.relationshipsByName
        var realObjectStructure:NSMutableDictionary = structureDictionary.mutableCopy() as NSMutableDictionary
        realObjectStructure.removeObjectForKey( "ManagedObjectName")
        for key in realObjectStructure.allKeys {
            // search for "ManagedObjectName" relationship entrys and delete them before filling the managedObject from this structure
            for relationshipName in relationshipsByName.allKeys {
                if relationshipName as NSString == key as NSString {
                    realObjectStructure.removeObjectForKey( key)
                }
            }
        }
        managedObject.setValuesForKeysWithDictionary( realObjectStructure)
        // the main object with attributes is created. Now care about the relationships
        for relationshipName in managedObject.entity.relationshipsByName.keys {
            var description:NSRelationshipDescription = relationshipsByName.objectForKey( relationshipName) as NSRelationshipDescription
            if !description.toMany {
                // to one relationship
                if parentObject == nil || description.destinationEntity != parentObject!.entity {
                    // no parent or relationship is "downward" -> recurse structure to add
                    var childStructureDictionary:NSDictionary = structureDictionary.objectForKey( relationshipName) as NSDictionary
                    if childStructureDictionary.count > 0 {
                        // dictionary not empty -> object must be created and added
                        var childObject:NSManagedObject? = self.managedObjectFromStructure( childStructureDictionary, moc: moc, parentObject: managedObject)
                        // validateForUpdate
                        var error:NSError?
                        if !managedObject.validateForUpdate( &error) {
                            println("Error: Object not in valid state for update!!! -> \(error)")
                        } else {
                            managedObject.setValue( childObject, forKey: relationshipName as NSString)
                        }
                    } else {
                        // relationship is "upward" -> nothing to do
                    }
                }
            } else {
                // to many relationship
                var relationshipSet:NSMutableSet = managedObject.mutableSetValueForKey( relationshipName as NSString)
                var relationshipArray:NSArray = structureDictionary.objectForKey( relationshipName as NSString) as NSArray
                for childStructureDictionary in relationshipArray {
                    if childStructureDictionary.count > 0 {
                        // dictionary not empty -> object must be created and added
                        var childObject:NSManagedObject = self.managedObjectFromStructure( childStructureDictionary as NSDictionary, moc: moc, parentObject: managedObject)
                        // validateForUpdate
                        var error:NSError?
                        if !managedObject.validateForUpdate( &error) {
                            println( "Error: Object not in valid state for update!!! -> \(error)")
                        } else {
                            relationshipSet.addObject( childObject)
                        }
                    } else {
                        // no object was behind the relationship -> nothing to do
                    }
                }
                // save set
                managedObject.setValue( relationshipSet, forKey: relationshipName as NSString)
            }
        }
        // final check validateForUpdate
        var error:NSError?
        if !managedObject.validateForUpdate( &error) {
            println( "Error: Object not in valid state for update although all previous check are passed!!! -> \(error)")
        }
        return managedObject
    } else {
        println( "Error: structure for object was empty. this should not happen at this point")
        var objectName:NSString = structureDictionary.objectForKey( "ManagedObjectName") as NSString
        var managedObject:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName( objectName, inManagedObjectContext: moc) as NSManagedObject
        return managedObject
    }
}

func dataStructuresFromManagedObjects( managedObjects: NSArray) -> NSArray {
    var dataArray:NSMutableArray = []
    for managedObject in managedObjects {
        dataArray.addObject( self.dataStructureFromManagedObject(managedObject as? NSManagedObject))
    }
    return dataArray
}

The key here is to pass the parent entity as argument to the recursion, so we can decide which relationship we have to fill with data. So the both functions: dataStructureFromManagedObject and managedObjectFromStructure can encode and decode any entity object from CoreData into a dictionary and back into an object.

MPajak
  • 99
  • 1
  • 7