[追記]
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にも対応したライブラリも公開されているので導入もめちゃめちゃ楽です。
manager.responseSerializer = [CUJSONResponseSerializer serializer];
NSNullを残しておきたい場合は、4を使用するのもいいと思います。
##個人的に求めてる解決方法
JSONパースするときnullの場合NSNullオブジェクトをセットしないオプションがあったらいいんじゃないかなって思います。
オブジェクトの順番や個数が必要な場合などNSNullでないといけないケースはもちろんありますが、必要でないケースもまた多くあるかなと。
パース後にどうするかという問題よりかはパーサーで何とかするべき問題じゃないかなって思うんですがどうなんでしょう。
JSONKitでもこことかここで話題になっていますが対応はされていません。何か問題あるのかな。(※リンク先では空文字列を返せとわけわかんないこと言ってますが)
##参考
JavaScript Object Notation - wikipedia
JSONにNSNullが入ってきたとき
NSArrayやNSDictionaryからNSNullを効率よく取り除く