【Swift5】 NSNumberのBoolとIntの判定(Alamofire)

3 min read読了の目安(約3300字

背景

Alamofireを使用してAPI通信を行うときに、
parameter(body)を[String:Any]に変換して渡してあげる必要がありました。
そのときに

  1. Codableでエンコードしてdata型に変換
  2. JsonSerializationを使用してdata→[String:Any]に変換

を行えば、簡単に変更ができます。
例えば以下の例です。

struct Todo: Codable {
  let id: String
  let title: String
  let number: Int
}

let todo: Todo = Model(id: "ssss", title: "hogehoge", number: 8)
let data: Data = JSONEncoder().encode(todo)
let parameter: [String:Any] = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(parameter)
// jsonに変換
// ["id": "ssss", "title": "hogehoge", "number": 8]

このようにModelがStringやIntだけであれば上記の流れで変換は可能です。

問題は以下のようにBool型が入ったときになります。

struct Todo: Codable {
  let id: String
  let title: String
  let number: Int
  let isDone: Bool
}

let todo: Todo = Model(id: "ssss", title: "hogehoge", number: 8, isDone: true)
let data: Data = JSONEncoder().encode(todo)
let parameter: [String:Any] = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(parameter)
// jsonに変換
// ["id": "ssss", "title": "hogehoge", "number": 8, "isDone": 1]
// isDoneがfalseなら0,trueなら1に変換されてしまう

上記のようにBool型がInt型のように変換されてしまうため、元の値がBoolだったのかIntだったのかが判別できないという問題にぶつかりました。

解決策

結論から言うと、一度上記の流れで変換したあとに
CFBooleanGetTypeIDCFGetTypeIDを取得しその値を比較することで、元の値がBoolだったのかIntだったかを判定しました。

コードとしては以下のような判定になります。

func isBoolNumber(number: NSNumber) -> Bool {
    let boolID = CFBooleanGetTypeID()
    let numID = CFGetTypeID(number)
    // 元の値がboolだとboolIDとnumIDが異なる値になる
    return numID == boolID
}

こちらが成り立つ理由としては、JsonSerializeで変換後の値に差異があるためです。
上記の変換後のnumberとisDoneの型と値に注目すると、

number: NSNumber 8
isDone: NSNumber YES

という風に元がBool値の場合,

trueの時はNSNumber YES
falseの時はNSNumber NO

のようになります。
こちらの違いを利用すると上記のように各種IDを取得すると値が異なるので
判定をかけることができます。

全体コード

自分の場合dictionaryという変数をmodelに用意して、変換を行うクロージャーを定義しました。

struct Todo: Codable {
    let id: String
    let title: String
    let number: Int
    let isDone: Bool

    var dictionary: [String: Any]? {
        guard let data = try? JSONEncoder().encode(self) else { return nil }
        return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { data in
            if let datas = data as? [String: Any] {
                return convertNSNumberToBool(value: datas)
            }
            return data as? [String: Any]
        }
    }
    // Boolの場合はBoolに変換を行う
    func convertNSNumberToBool(value: [String: Any]) -> [String: Any] {
        let array = value.mapValues { value -> Any in
	    // 値がNSNumberの場合は判定の関数を行う
            if value is NSNumber {
                if isBoolNumber(number: value as! NSNumber) {
                    return value as! Int == 0 ? false : true
                }
	    // jsonの階層は2階層以上の場合に対応
            } else if value is [String: Any] {
                return convertNSNumberToBool(value: value as! [String: Any])
            }
            return value
        }
        return array
    }
    // NSNumberがboolかintか判定を行う
    func isBoolNumber(number: NSNumber) -> Bool {
        let boolID = CFBooleanGetTypeID()
        let numID = CFGetTypeID(number)
        return numID == boolID
    }
}

上記のやり方だと

  1. Codableでエンコード
  2. JsonSerializeでjsonに変換
  3. jsonのデータをmap関数で一つ一つを判定し、代入し直す

のように一度変換したものを無理矢理値を直しているため、あまりスマートなやり方ではないです。
いろいろ調べましたが、上記の方法でないとできなかったので
もっとスマートなやり方があればぜひ教えて下さい・・。