3

Run this code:

NSData *jsonData = [@"{\"foo\":\"bar\"}" dataUsingEncoding:NSUTF8StringEncoding];
id result = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
if ([result isKindOfClass:[NSMutableDictionary class]])
{
    NSMutableDictionary *dict = (NSMutableDictionary *)result;
    [dict setObject:@"foo" forKey:@"baz"];
}

And you get this exception from the setObject call:

* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[__NSCFDictionary setObject:forKey:]: mutating method sent to immutable object'

When parsing JSON with a top-level dictionary object, NSJSONSerialization returns an immutable dictionary object that says it's a an NSMutableDictionary but throws an exception when you try to mutate it.

I know I can pass the NSJSONReadingMutableContainers option to get mutable containers. But what is going on here under the hood? Why does a class say it's a mutable subclass but then behave as if it's immutable?

Edit to clarify question: I know there are several ways to make this code work. I could call mutableCopy on the result or use the NSJSONReadingMutableContainers option to get mutable results. The point here is that an object says its type is NSMutableDictionary or a subclass thereof, and then it causes a crash when I try to mutate it. Why does this behavior exist? Should it be considered a bug? Or is this some expected quirk of the Objective-C runtime?

Tom Hamming
  • 10,577
  • 11
  • 71
  • 145
  • I am not subclassing `NSDictionary` or `NSMutableDictionary`. – Tom Hamming Jul 02 '14 at 23:31
  • You are confusing "subclass of" and "instance of" in your question title. – Fogmeister Jul 02 '14 at 23:40
  • 3
    Take a look at the section on **Object Mutability** in Apple's [Concepts in Objective-C Programming](https://developer.apple.com/library/mac/documentation/general/conceptual/CocoaEncyclopedia/ObjectMutability/ObjectMutability.html) guide. One of the subheads reads: **Use Return Type, Not Introspection**. Here's an excerpt: "[Y]ou should not make assumptions about whether an object is mutable based on class membership. Your decision should be guided solely by what the signature of the method vending the object says about its mutability." – jlehr Jul 02 '14 at 23:40
  • Class clusters may seem weird, but they allow Apple's engineers to do some very valuable performance optimizations on heavily used classes. – jlehr Jul 02 '14 at 23:47
  • The return type from NSJSONSerialization, in the absence of the mutable option, will ALWAYS be non-mutable. – Hot Licks Jul 03 '14 at 00:29
  • (Understand that the difference between a mutable and non-mutable NSDictionary/NSArray is merely a flag in the header. Very often the object starts out mutable and is "flipped" in the `init` routine after initialization is complete. Most likely NSJSONSerialization makes use of this feature.) – Hot Licks Jul 03 '14 at 00:32
  • @jlehr - It is unfortunate that the mutable/non-mutable classes do not provide an `isMutable` interface or whatever, to allow this to be tested at runtime. – Hot Licks Jul 03 '14 at 00:36

1 Answers1

4

Because you're trying to set the object for key on result (your NSCFDictionary -- immutable) not dict (your cast NSMutableDictionary).

Instead, try:

NSData *jsonData = [@"{\"foo\":\"bar\"}" dataUsingEncoding:NSUTF8StringEncoding];
id result = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil];
if ([result isKindOfClass:[NSDictionary class]])
{
    NSMutableDictionary *dict = [result mutableCopy];
    [dict setObject:@"foo" forKey:@"baz"];
}

The reason result is passing as true when you're trying to determine if it fits NSMutableDictionary class, is because NSCFDictionary is a class cluster that matches both NSDictionary and NSMutableDictionary. (see What is an NSCFDictionary?).

In essence (in that if statement), you aren't actually checking at all whether the NSCFDictionary is mutable or not -- you're simply checking that it is a subclass (or member class) of NSMutableDictionary.

Community
  • 1
  • 1
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • But why is he getting into that if? Then it should already be a NSMutableDictionary. – Prine Jul 02 '14 at 22:54
  • It's the same object, so it doesn't matter. But I'll update the question to address that oddity. – Tom Hamming Jul 02 '14 at 22:55
  • 1
    I was in the process of an edit with those details - see update – brandonscript Jul 02 '14 at 22:56
  • It still crashes with the same exception if I do it with `dict` instead. – Tom Hamming Jul 02 '14 at 22:56
  • A cast operator can't change an object's type at runtime, so the cast in your example won't make the code behave any differently than it did before. Instead, you send send a `mutableCopy` message to the original dictionary, and work with the mutable instance. – jlehr Jul 02 '14 at 23:01
  • Ah! It does at that. Almost missed it. You can't simply cast immutable to mutable; you have to create a mutableCopy. See updated code. – brandonscript Jul 02 '14 at 23:02
  • Nope, make a mutable copy of `result`. – jlehr Jul 02 '14 at 23:02
  • The point though is that this seems to break the assumption that any subclass of class X will do anything that X will. – Tom Hamming Jul 02 '14 at 23:03
  • @Mr.Jefferson tbh, it feels more like a hack for Objective-C to handle JSON via NSCFDictionary. It is what it is. When in doubt, explicitly cast everything, or do an if/else to check that the resulting JSON is NSDictionary or NSArray kind of class. – brandonscript Jul 02 '14 at 23:06
  • Apple hides the implementation details of class clusters, but in this the current implementation uses instances of `NSCFDictionary` to represent both mutable and immutable instances. The `NSCFDictionary` may be using an internal state flag to determine whether it's mutable, though I'm not sure of that. So `isKindOfClass` responds `YES` for both types. Class clusters can be a little weird. – jlehr Jul 02 '14 at 23:07
  • @remus Actually, no, it's better to take the opposite approach -- avoid using casts in Objective-C, because they mask the reality of what's going on, and don't check for mutability because that involves looking for private implementation details that are essentially unknowable. – jlehr Jul 02 '14 at 23:10
  • @jlehr I was trying to convey that lol.. I meant "don't cast it", check if it's a dictionary or array, then create a mutable copy if required. – brandonscript Jul 02 '14 at 23:10
  • @remus Also, please correct your example -- it doesn't make sense at the moment. You're sending `mutableCopy` to an immutable dictionary *after* you try to mutate it and ignoring the mutable dictionary that's returned. As I said earlier, you need to send `mutableCopy` to `result`, not `dict`. – jlehr Jul 02 '14 at 23:12
  • @remus Don't forget to remove the cast. It's *really* pointless now. – jlehr Jul 02 '14 at 23:14