🍁
Swift: 非オプショナルな変数のキーがなくてもデフォルト値で初期化してくれるCodable
Codable (Encodable & Decodable)
を使えば、階層構造を持つデータ(JSONオブジェクトなど)を簡単に扱うことができます。しかし、JSON文字列からSwiftのデータオブジェクトにするとき、少し気をつけないといけない点があります。
オプショナルな変数の場合、JSONオブジェクトの中にそのKeyとValueがなくても、nil
でデコードしてくれますが、非オプショナルな変数の場合は、変数に初期値を与えていてもデコードに失敗します。
例
class Hoge: Codable {
var piyo: String = "none"
var meu: String?
}
let str1 = """
{
"piyo": "hello"
}
"""
if let jsonData = str1.data(using: .utf8),
let json = try? JSONDecoder().decode(Hoge.self, from: jsonData) {
print(json.piyo) // デコードに成功する
// => hello nil
}
let str2 = "{}" // piyoのkeyとvalueがない
if let jsonData = str2.data(using: .utf8),
let json = try? JSONDecoder().decode(Hoge.self, from: jsonData) {
print(json.piyo) // デコードに失敗する
// => 出力されない
}
これをデコード失敗しないようにするには、CodingKey
を定義してrequired init(from decoder: Decoder) throws {}
を実装すれば良いです。このとき、キーが存在しない可能性を考慮する場合はdecodeIfPresent()
を使います。
上記の例を改良
class Hoge: Codable {
var piyo: String = "none"
var meu: String?
enum CodingKeys: String, CodingKey {
case piyo
case meu
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
piyo = try values.decodeIfPresent(String.self, forKey: .piyo) ?? piyo
meu = try? values.decodeIfPresent(String.self, forKey: .meu)
}
}
let str = "{}" // piyoのkeyとvalueがない
if let jsonData = str.data(using: .utf8),
let json = try? JSONDecoder().decode(Hoge.self, from: jsonData) {
print(json.piyo, json.meu) // デコードに成功する
// => none nil
}
ただ、この対応策はCodable
なclass
やstruct
のそれぞれで実装する必要があり、毎回 変数名 = try values.decodeIfPresent(型.self, forKey: キー) ?? 変数名
と決まった書き方を繰り返すのは手間です。
そこで、キーが見つからなくてデコードに失敗する場合はデフォルト値を使って初期化できる方法を考えました。
public protocol EmptyInitializable {
init()
}
public typealias EmptyCodable = Decodable & Encodable & EmptyInitializable
public extension Encodable {
func toJSONString() -> String? {
guard let jsonData = try? JSONEncoder().encode(self) else {
return nil
}
return String(data: jsonData, encoding: .utf8)
}
}
public extension Decodable {
init?(jsonString: String) {
guard let jsonData = jsonString.data(using: .utf8),
let json = try? JSONDecoder().decode(Self.self, from: jsonData) else {
if let type = Self.self as? EmptyCodable.Type {
self = type.init() as! Self
return
} else {
return nil
}
}
self = json
}
}
class Hoge: EmptyCodable {
var piyo: String = "none"
var meu: String?
required init() {}
}
※ Encodableの実装はおまけです。
要点
-
init()
だけをもつprotocol = EmptyCodable
を定義します。 -
EmptyCodable
とDecodable
とEncodable
をかけあわせたprotocol
のtypealias
を定義します。 -
Decodable
を拡張して、JSON文字列からデータオブジェクトをデコードするイニシャライザを定義します。- デコードに失敗したが、
EmptyCodable
に準拠している場合は、そのイニシャライザを利用するようにします。
- デコードに失敗したが、
これで、もしもJSONオブジェクトが{}
のように空っぽだったとしても、初期値を持つデータオブジェクトに変換できます。留意点として、複数の変数があってそのうちのいくつかのキーが見つからなかった時の個々の処理には対応できていません。その場合はやはり個々にデコード処理を定義する必要があると思います。
Discussion