Edited at

iOSでパース後のJSONオブジェクトにNSNullが含まれている場合の各種対処方法まとめ

More than 3 years have passed since last update.


[追記]

AFNetworking (2.4.0)removesKeysWithNullValuesが導入されました。

これでResponseSerializerで簡単にNullを削除することができます。めでたしめでたし。



前提知識

iOSではNSJSONSerializationクラスでJSONをパースすることができます。

NSError *error = nil;

id json = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingAllowFragments
error:&error];

JSONで表現できるデータ型と対応するObjective-Cの型は以下の通りです。

JSONのデータ型
Objective-C

数値(整数、浮動小数点数)
NSNumber

文字列(バックスラッシュによるエスケープシーケンス記法を含む、ダブルクォーテーションでくくった文字列)
NSString

真偽値(trueとfalse)
NSNumber

配列(データのシーケンス)
NSArray

オブジェクト(順序づけされていないキーと値のペアの集まり。JSONでは連想配列と等価)
NSDictionary

null
NSNull

※ nullがnilではなくてNSNullオブジェクトというところに注意して下さい。NSArrayやNSDictionaryにはnilは格納できないのでNSNullオブジェクトである必要があります。


NSNullの何が問題?

例えば、以下のような文字列が1文字以上ある場合に何らかの処理をするコードがあるとします。

NSString *text = [json objectForKey:@"text"];

if (text.length > 0) {
// something
}

objectForKey:で返される値がNSStringクラスだったら問題ないのですが、元の値がnullだった場合にNSNullが返されることになるのでtextはNSNullオブジェクトになります。

その場合、text.lengthで例外("unrecognized selector sent to instance.")が発生しクラッシュします。これはNSNullクラスにはlengthメソッドが実装されていないため発生する例外です。

このような例外を防ぐために、

if ([text isKindOfClass:[NSString class]] && text.length > 0) {

// something
}

or

if ([text respondsToSelector:@selector(length)] && text.length > 0) {

// something
}

といった、メソッドを呼び出す前にクラスやセレクタを確認することによって例外を防ぐのですが、いちいち確認するのは煩雑過ぎます。

通常は上記の様に呼び出し毎に確認するのではなく、モデル等の初期化で確認してその後安全に値にアクセスする方法を取ると思うのですが、やはり確認するコードは煩雑になります。


対処方法

クラスやセレクタを確認せずに例外を防ぎたいです。

nilだったら上記の例外は発生しません。

ということで対処方法の大筋はnilを利用するかNSNullを削除する方法になります。


各種対処方法


1. NSNullをnilとして振る舞わせる

NullSafe


実装

NSNullで例外("unrecognized selector sent to instance.")を発生させないようにしている。



導入方法

pod 'NullSafe'



使用方法

クラスファイルをプロジェクトに含ませれば自動的に有効になる



注意点

プロジェクト全体でNSNullの振る舞いが変更されるのでかなり注意が必要です。



利点

現状のプロジェクトを一切いじることなくNSNullの例外を防ぐことができます。(※注意点の問題がない場合)



2. NSNullを一括で削除する

ISRemoveNull


実装

NSArray, NSDictionaryの全ての要素にアクセスしてNSNullを削除する。



導入方法

pod 'ISRemoveNull'



使用例

NSDictionary *strippedDictionary = [dictionary dictionaryByRemovingNull];


NSJSONSerialization-NSNullRemoval


実装

NSJSONSerializationのカテゴリに実装されているが、やっていることはISRemoveNullと同じ。



導入方法

pod 'NSJSONSerialization-NSNullRemoval', :git => 'https://github.com/jrturton/NSJSONSerialization-NSNullRemoval'



使用例

stripped = [NSJSONSerialization JSONObjectWithData:data

options:NSJSONReadingMutableContainers
error:nil
removingNulls:YES
ignoreArrays:NO];



注意点

削除するために全てのオブジェクトにアクセスする必要があるので要素数やネストが深さに比例してコストがかかる



利点

NSArray, NSDictionaryやパース時に個別で削除するかを選択できる。



3. NSNullを効率よく削除する

CollectionUtils


実装

2を改良したもの。2ではネストも含む全てのオブジェクトにアクセスしていたのに対してこちらは初期化ではトップのオブジェクトに対してのみNSNullの確認と削除をし、ネストしたオブジェクトへは初めてアクセスしたときに確認して削除する。



導入方法

pod "CollectionUtils"



使用例

NSArray *array = @[@"0", @"1", [NSNull null], @"2", [NSNull null], @"3"];

NSArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", "3"]
//alloc/initを使って生成することも可能


利点

3よりも初期化時のコストを抑えることができる。

AFNetworkingに対応しているので(後述)導入が簡単。

詳しい解説

NSArrayやNSDictionaryからNSNullを効率よく取り除く


4. NSNullの場合にnilを返す


実装例

// Categoryにnilを返すメソッドを追加

@implementation NSDictionary (Additions)

- (id)objectOrNilForKey:(id)aKey
{
id obj = [self objectForKey:aKey];
if (obj == [NSNull null]) {
return nil;
} else {
return obj;
}
}

@end



使用例

NSString *text = [json objectOrNilForKey:@"text"];



注意点

objectForKey:を置き換える必要がある。



利点

アクセス毎にNSNullかnilを利用するか選択できる。



結論

NSNullを削除したければ3のCollectionUtilsが現状だと最適だと思いました。AFNetworking 2.xのresponseSerializerにも対応したライブラリも公開されているので導入もめちゃめちゃ楽です。


CollectionUtils-AFNetworking

manager.responseSerializer = [CUJSONResponseSerializer serializer];


NSNullを残しておきたい場合は、4を使用するのもいいと思います。


個人的に求めてる解決方法

JSONパースするときnullの場合NSNullオブジェクトをセットしないオプションがあったらいいんじゃないかなって思います。

オブジェクトの順番や個数が必要な場合などNSNullでないといけないケースはもちろんありますが、必要でないケースもまた多くあるかなと。

パース後にどうするかという問題よりかはパーサーで何とかするべき問題じゃないかなって思うんですがどうなんでしょう。

JSONKitでもこことかここで話題になっていますが対応はされていません。何か問題あるのかな。(※リンク先では空文字列を返せとわけわかんないこと言ってますが)


参考

JavaScript Object Notation - wikipedia

JSONにNSNullが入ってきたとき

NSArrayやNSDictionaryからNSNullを効率よく取り除く