Removing nulls from a JSON structure recursively
Asked Answered
K

4

5

I'm frequently finding the need to cache data structures created by NSJSONSerialization to disk and as -writeToFile fails if there are nulls, I need a fix that works when the structure is unknown. This works, and direct mutation is allowed as the instances of NSMutableDictionary themselves are not being enumerated, but it feels a bit hacky.

Is this totally fine or is it absolutely necessary to recreate a new tree and return that?

- (void) removeNullsFromJSONTree:(id) branch
{
    if ([branch isKindOfClass:[NSMutableArray class]])
    {
        //Keep drilling to find the leaf dictionaries
        for (id childBranch in branch)
        {
            [self removeNullsFromJSONTree:childBranch];
        }
    }
    else if ([branch isKindOfClass:[NSMutableDictionary class]])
    {
        const id nul = [NSNull null];
        const NSString *empty = @"";
        for(NSString *key in [branch allKeys])
        {
            const id object = [branch objectForKey:key];
            if(object == nul)
            {
                [branch setObject:empty forKey:key];
            }
        }
    }
}
Kaseykasha answered 1/5, 2013 at 4:48 Comment(3)
if([object isKindOfClass:[NSNull null]]) try like thisShem
Yes origionally it was testing with isKindOfClass but as stated in similar SO question about removing nulls (which i can't seem to find), comparing pointers to constants is more efficient.Kaseykasha
I made a category which does that. You can find it here github.com/bismasaeed00/NullReplacerTintinnabulation
I
14

There's nothing wrong with your general approach. Since NSNull is a singleton, it's fine to look for it by pointer comparison.

However, you're not recursing on the values in your dictionary. In general, those values might be arrays or dictionaries themselves. Perhaps in your specific case you know they're not. But if they could be, you need to perform removeNullsFromJSONTree: on each value in the dictionary.

You also don't look for NSNull in an array. Should you? It's trivial to handle:

[branch removeObject:[NSNull null]];

The removeObject: method removes all instances of the argument.

Personally I don't like testing object classes explicitly when I can use categories to let the message sending system do it for me. So instead I might define a category on NSObject like this:

// NSObject+KezRemoveNulls.h

@interface NSObject (KezRemoveNulls)

- (void)Kez_removeNulls;

@end

I would write a default do-nothing implementation for NSObject, and override it for NSMutableArray and NSMutableDictionary:

// NSObject+KezRemoveNulls.m

#import "NSObject+KezRemoveNulls.h"

@implementation NSObject (KezRemoveNulls)

- (void)Kez_removeNulls {
    // nothing to do
}

@end

@implementation NSMutableArray (KezRemoveNulls)

- (void)Kez_removeNulls {
    [self removeObject:[NSNull null]];
    for (NSObject *child in self) {
        [child Kez_removeNulls];
    }
}

@end

@implementation NSMutableDictionary (KezRemoveNulls)

- (void)Kez_removeNulls {
    NSNull *null = [NSNull null];
    for (NSObject *key in self.allKeys) {
        NSObject *value = self[key];
        if (value == null) {
            [self removeObjectForKey:key];
        } else {
            [value Kez_removeNulls];
        }
    }
}

@end

Note that all of the implementation code is still in one file.

Now I could say this:

id rootObject = [NSJSONSerialization JSONObjectWithData:...];
[rootObject Kez_removeNulls];
Intendment answered 1/5, 2013 at 5:22 Comment(7)
I just wonder if Avoid Category Method Name Clashes applies here: "If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined ...".Shevat
I believe in this case it's fine, because I defined the same category (KezRemoveNulls) for super and subclasses. Your quote says “or a method in another category”.Intendment
You are right, I had overlooked that part, thanks for the feedback! - (And now that I think of it: I did the same thing with categories on NSManagedObject :-)Shevat
Realistically, I'm pretty sure this works, and used to be better explained in some documentation Apple has unfortunately removed from their website. (See this answer.). This would be a reasonable question to ask Greg Parker at WWDC if the opportunity arises.Intendment
I was lucky to get a ticket within the 2 minutes :-) But I must admit that I don't know Greg Parker.Shevat
@robmayoff Wow thanks for the great answer. Recursing on the values with the empty method for NSObject is really nice. I was originally avoiding removing objects as I knew the leaves would always be strings destined for UILabels. Removing and testing for their existance later is much better in the long run.Kaseykasha
I checked with Greg Parker at WWDC 2013 and he says this is 100% kosher.Intendment
E
10

Here's the code I'm using to clean up my JSON calls, seems to work well but, since there's some processing overhead involved I really only use it in situations where I can't do the null handling on the server. NSNull crashes is far and away our biggest app crash problem.

+ (id)cleanJsonToObject:(id)data {
    NSError* error;
    if (data == (id)[NSNull null]){
        return [[NSObject alloc] init];
    }
    id jsonObject;
    if ([data isKindOfClass:[NSData class]]){
        jsonObject = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
    } else {
        jsonObject = data;
    }
    if ([jsonObject isKindOfClass:[NSArray class]]) {
        NSMutableArray *array = [jsonObject mutableCopy];
        for (int i = array.count-1; i >= 0; i--) {
            id a = array[i];
            if (a == (id)[NSNull null]){
                [array removeObjectAtIndex:i];
            } else {
                array[i] = [self cleanJsonToObject:a];
            }
        }
        return array;
    } else if ([jsonObject isKindOfClass:[NSDictionary class]]) {
        NSMutableDictionary *dictionary = [jsonObject mutableCopy];
        for(NSString *key in [dictionary allKeys]) {
            id d = dictionary[key];
            if (d == (id)[NSNull null]){
                dictionary[key] = @"";
            } else {
                dictionary[key] = [self cleanJsonToObject:d];
            }
        }
        return dictionary;
    } else {
        return jsonObject;
    }
}

You call it by passing the NSData retrieved via NSURLConnection.

NSArray *uableData = [utility cleanJsonToObject:data];

or

NSDictionary *uableData = [utility cleanJsonToObject:data];
Epochmaking answered 30/8, 2013 at 19:46 Comment(1)
Great! I was looking for something similar. You saved my time of writing my own method. Thanks @Travis :)Gerontology
P
0

Swift 4 version of @Travis M.'s answer;

class func removeNullFromJSONData(_ JSONData: Any) -> Any {
    if JSONData as? NSNull != nil {
        return JSONData
    }

    var JSONObject: Any!

    if JSONData as? NSData != nil {
        JSONObject = try! JSONSerialization.data(withJSONObject: JSONData, options: JSONSerialization.WritingOptions.prettyPrinted)
    }
    else {
        JSONObject = JSONData
    }

    if JSONObject as? NSArray != nil {
        let mutableArray: NSMutableArray = NSMutableArray(array: JSONObject as! [Any], copyItems: true)
        let indexesToRemove: NSMutableIndexSet = NSMutableIndexSet()

        for index in 0 ..< mutableArray.count {
            let indexObject: Any = mutableArray[index]

            if indexObject as? NSNull != nil {
                indexesToRemove.add(index)
            }
            else {
                mutableArray.replaceObject(at: index, with: removeNullFromJSONData(indexObject))
            }
        }

        mutableArray.removeObjects(at: indexesToRemove as IndexSet)

        return mutableArray;
    }
    else if JSONObject as? NSDictionary != nil {
        let mutableDictionary: NSMutableDictionary = NSMutableDictionary(dictionary: JSONObject as! [AnyHashable : Any], copyItems: true)

        for key in mutableDictionary.allKeys {
            let indexObject: Any = mutableDictionary[key] as Any

            if indexObject as? NSNull != nil {
                mutableDictionary.removeObject(forKey: key)
            }
            else {
                mutableDictionary.setObject(removeNullFromJSONData(indexObject), forKey: key as! NSCopying)
            }
        }

        return mutableDictionary
    }
    else {
        return JSONObject
    }
}
Pocahontas answered 30/3, 2018 at 15:53 Comment(0)
P
-1
+ (id)getObjectWithoutNullsForObject:(id)object
{
    id objectWithoutNulls;

    if ([object isKindOfClass:[NSDictionary class]])
    {
        NSMutableDictionary *dictionary = ((NSDictionary *)object).mutableCopy;

        [dictionary removeObjectsForKeys:[dictionary allKeysForObject:[NSNull null]]];

        for (NSString *key in dictionary.allKeys)
        {
            dictionary[key] = [self getObjectWithoutNullsForObject:dictionary[key]];
        }

        objectWithoutNulls = dictionary;
    }
    else if ([object isKindOfClass:[NSArray class]])
    {
        NSMutableArray *array = ((NSArray *)object).mutableCopy;

        [array removeObject:[NSNull null]];

        for (NSUInteger index = 0; index < array.count; index++)
        {
            array[index] = [self getObjectWithoutNullsForObject:array[index]];
        }

        objectWithoutNulls = array;
    }
    else if ([object isKindOfClass:[NSNull class]])
    {
        objectWithoutNulls = Nil;
    }
    else
    {
        objectWithoutNulls = object;
    }

    return objectWithoutNulls;
}
Pteridophyte answered 31/10, 2016 at 13:23 Comment(3)
@Huperniketes -> 2) Setting array and dictionary elements to Nil defaults to an exception being thrown. -> This can never happen if you run the above code.Pteridophyte
@Huperniketes -> 3) Indexing arrays is very inefficient. Use fast enumeration instead. -> An object cannot be reassigned to the actual array if fast enumeration is used.Pteridophyte
1) While you do understand recursion well enough, ObjC is an object-oriented language and you fail to use polymorphism to hide details of other classes from your code. Replace the use of -isKindOfClass: with the appropriate methods in their respective classes. -> It is not always necessary to use polymorphism.Pteridophyte

© 2022 - 2024 — McMap. All rights reserved.