記事の背景
先日、API にリクエストを投げる際、Int?
型に nil を入れて投げたところ、うまく反映されないという問
題にぶち当たった。
背景の詳細説明
上記の説明だと少しわかりづらいと思い、サンプルコードを使って説明する。
ぜひ、Swift Playground などで試して欲しい。
以下のような JSON データがあったとする。
{
"name" : "Ame",
"age" : 18,
"hobby" : "web制作"
}
この場合、Swift の構造体から JSON データに変換するためには対象の構造体が Encodable
プロトコルまたは Codable
プロトコルに準拠している必要がある。
そのため、以下のような構造体を作成する。
struct Person: Encodable {
var name:String
var age:Int?
var hobby:String?
}
ここで、age
と hobby
は Null を許容するものとした。
これらを基に、Swift で JSON データにエンコードする。JSON データをエンコードするには JSONEncoder
を使用する必要がある。
そのため、以下のようなコードを作成する。
struct Person: Encodable {
var name:String
var age:Int?
var hobby:String?
}
+ let person = Person(name: "Ame", age: 21, hobby: "Watch TV")
+
+ let encoder = JSONEncoder()
+
+ // フォーマットを指定
+ encoder.outputFormatting = .prettyPrinted
+
+ // エンコード
+ let jsonData = try encoder.+ encode(person)
+
+ // 文字コードUTF8のData型に変換
+ print(String(data: jsonData , encoding: .utf8)!)
これを実行すると、以下の JSON 形式のコードが出力される。
{
"name" : "Ame",
"age" : 21,
"hobby" : "Watch TV"
}
ここまではよくあるエンコードの話。
次に、age
と hobby
に nil
を入れてリクエストを投げてみる。
import Foundation
struct Person: Codable {
var name:String
var age:Int?
var hobby:String?
}
- let person = Person(name: "Ame", age: 21, hobby: "Watch TV")
+ let person = Person(name: "Ame", age: nil, hobby: nil)
let encoder = JSONEncoder()
// フォーマットを指定
encoder.outputFormatting = .prettyPrinted
// エンコード
let jsonData = try encoder.encode(person)
// 文字コードUTF8のData型に変換
print(String(data: jsonData , encoding: .utf8)!)
ここで、理想的な出力は、以下のようになる。
{
"name" : "Ame",
"age" : null,
"hobby" : null
}
しかし実行すると、以下のように出力される。
{
"name" : "Ame"
}
このように、nil
を入れた箇所だけ空白で出力される。
この実際の出力をどうにかして理想的な出力の形にしたいというのが今回の記事の目的である。
本論
あえていきなり結論を言うと、JSONEncoder
がキーを保持し、null
をセットするように設定する方法しかない。
自動生成されたものは使えないため、自分で encode(to:)
の実装を書く必要がある。
明示的にフィールド内をエンコード処理させると、以下のようなコードになる。
import Foundation
struct Person: Codable {
var name:String
var age:Int?
var hobby:String?
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(name, forKey: .name)
+ try container.encode(age, forKey: .age)
+ try container.encode(hobby, forKey: .hobby)
+ }
}
let person = Person(name: "Ame", age: nil, hobby: nil)
let encoder = JSONEncoder()
// フォーマットを指定
encoder.outputFormatting = .prettyPrinted
// エンコード
let jsonData = try encoder.encode(person)
// 文字コードUTF8のData型に変換
print(String(data: jsonData , encoding: .utf8)!)
{
"name" : "Ame",
"age" : null,
"hobby" : null
}
しかし、これだと他の箇所でも同じ内容のコードを追加することになり、不便なので、プロパティラッパーを作成した方が便利そうではある。
よって、以下のようなコードを作成する。(ただし、Swift5.1 以上が必要)
@propertyWrapper
struct NullEncodable<T>: Encodable where T: Encodable {
var wrappedValue: T?
init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch wrappedValue {
case .some(let value): try container.encode(value)
case .none: try container.encodeNil()
}
}
}
コードは比較的単純で、エンコードをする値があればそのままエンコード、なければ Null
値をエンコードしている。
使い方
プロパティがラップされた構造体は、プロパティの直前に属性を指定する必要がある。今回は @NullEncodable
を Null
値が必要なプロパティ(age
, hobby
)の直前に指定する必要がある。
import Foundation
struct Person: Codable {
var name:String
- var age:Int?
- var hobby:String?
+ @NullEncoda var age:Int?
+ @NullEncoda var hobby:String?
}
+@propertyWrapper
+struct NullEncodable<T>: Encodable where T: Encodable {
+
+ var wrappedValue: T?
+
+ init(wrappedValue: T?) {
+ self.wrappedValue = wrappedValue
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch wrappedValue {
+ case .some(let value): try container.encode(value)
+ case .none: try container.encodeNil()
+ }
+ }
+}
let person = Person(name: "Ame", age: nil, hobby: nil)
let encoder = JSONEncoder()
// フォーマットを指定
encoder.outputFormatting = .prettyPrinted
// エンコード
let jsonData = try encoder.encode(person)
// 文字コードUTF8のData型に変換
print(String(data: jsonData , encoding: .utf8)!)
{
"name" : "Ame",
"age" : null,
"hobby" : null
}
これで理想的なリクエストを送ることができた。
原因の考察
Apple、FoundationフレームワークのGitHubのとあるIssueによると、以下 3 つが挙げられる。
-
nil
をNull
でエンコードするオプションはencode(to:)
で自分で書くことができるから、Swift やフレームワークが用意する必要性はない - Null と欠落を同一視する API が多いため
- ペイロードは小さい方が良いから
以上の理由からこのような仕様になっていると考えられる。
まとめ
Swift5 では API にリクエストを投げる際、Int?
や String?
型に nil
を入れてもラベルが省略され、空欄となる。
そのため、JSONEncoder
で NUll
を入れるよう設定する必要があった。
参考文献