soranoba
soranoba Author of soranoba.net
programming

SwiftのDecodableなenumで将来的な値追加に対応する

Swift4から追加されたDecodableは, APIのレスポンスモデルを定義する際にとても便利な機能です.
しかし, 将来的に値が増える可能性のあるenumの場合には注意が必要です.

import Foundation

let decoder = JSONDecoder()

enum Enum: String, Decodable, CaseIterable { 
    case a 
    case b
} 

print(try decoder.decode([Enum].self, from: """
["a", "b", "c"]
""".data(using: .utf8)!))

例えば上記のコードを実行すると, Cannot initialize Enum from invalid String value c となって例外が投げられます.
しかし, typeのようなフィールドの場合, サポートされていない物だけを後でフィルタしたいといったことは往々にして存在します.
そんな時の対応方法です.

init?(rawValue:)を実装する

import Foundation

let decoder = JSONDecoder()

enum Enum: String, Decodable, CaseIterable { 
    case a 
    case b 
    case unspecified 
    init?(rawValue: String) { 
        self = type(of: self).allCases.first { $0.rawValue == rawValue } ?? .unspecified 
    } 
} 

print(try decoder.decode([Enum].self, from: """
["a", "b", "c"]
""".data(using: .utf8)!))

この方法はCaseIterableを用いて対応していないrawValueの時にfallbackするようなinit?(rawValue:)を実装することで実現します.
decode以外の場合にも影響する点には注意が必要ですが, シンプルで分かりやすい方法ではないでしょうか.

init(from decoder:) throwsを実装する

import Foundation

let decoder = JSONDecoder()

enum Enum: String, Decodable { 
    case a 
    case b 
    case unspecified 
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawValue = try container.decode(RawValue.self)
        self = type(of: self).init(rawValue: rawValue) ?? .unspecified
    }
} 

print(try decoder.decode([Enum].self, from: """
["a", "b", "c"]
""".data(using: .utf8)!))

この場合だとinit?(rawValue:)自体はデフォルトのままなので, Enum(rawValue: "c")とやった場合はnilになりますが, Decoderを使った場合は.unspecifiedになります.
正しく実装するのであれば, こちらでしょうか.

enumを要素に持つstructで対応する

import Foundation

let decoder = JSONDecoder()

struct Res: Decodable {
    enum Enum: String, Decodable { 
        case a 
        case b 
        case unspecified
    }
    private enum CodingKeys: String, CodingKey {
        case typeRaw="type"
    }

    var type: Enum {
        return Enum(rawValue: self.typeRaw) ?? .unspecified
    }
    private let typeRaw: Enum.RawValue
}

print((try decoder.decode(Res.self, from: """
{"type": "c"}
""".data(using: .utf8)!)).type)

あまり綺麗ではありませんが, レスポンスによってfallbackの有無を分けたい場合に使えるかもしれません.


こんなところでしょうか.

(Updated: )

comments powered by Disqus