Optional&Decodable
로 정의된 enum을 decoding할 때, case로 정의되지 않은 String이 들어있는 경우 에러가 나게 된다.
원래 Decodable에서는 init(from decoder: Decoder)
을 정의해줘야하는데, Codable property로 이루어진 타입인 경우에는 자동으로 해당 함수가 생성된다. 이 때, 프로퍼티가 optional인 경우에는 decodeIfPresent
를, non-optional인 경우에는 decode
를 호출한다. decodeIfPresent는 단순히 해당 키의 존재유무(그리고 값의 null 유무)만 확인한 후 나머지 프로세스는 decode와 동일하기 때문에, 키가 존재하지만, decoding이 실패하는 경우는 잡아내지 못하는 것.
가장 쉬운 해결책은 직접 Decodable을 구현해주는 것이다. (참고로 Codable은 Encodable과 Decodable을 포함하는 프로토콜이다.)
enum FruitType: String, Codable {
case orange, banana, strawberry
case unknown
init(from decoder: Decoder) throws {
self = try? FruitType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}
이 때 try?
를 사용하여 (case가 존재하지 않는 경우를 포함하여) decoding에 실패할 경우 default case를 지정해줄 수 있다. 단, 실패할 경우 nil을 할당해줄 수는 없다.)
Property Wrapper는 property가 저장될 때의 제약을 @wrapper-name
과 같은 형식으로 property 정의시 추가할 수 있는 방법이다.
@propertyWrapper
struct SkipIfFailed<T: Codable>: Codable {
let wrappedValue: T?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(T.self)
}
}
struct MyDataModel: Codable {
@SkipIfFailed
var type: FruitType?
}
var
로 정의해야 property wrapper를 사용할 수 있다.@propertyWrapper
struct SkipFailedElement<T: Codable>: Codable {
let wrappedValue: [T]?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let elements = try? container.decode([SkipIfFailed<T>].self)
guard let elements = elements else {
wrappedValue = nil
return
}
wrappedValue = elements.compactMap { $0.wrappedValue }
}
}
struct MyDataModel: Codable {
@SkipFailedElement
var types: [FruitType]?
}
protocol CodableWithUnknown: Codable {}
extension CodableWithUnknown where Self: RawRepresentable, Self.RawValue == String {
init(from decoder: Decoder) thorws {
do {
try self = Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))!
} catch {
if let unknown = Self(rawValue: "unknown") {
self = unknown
} else {
throw error
}
}
}
}
enum SomeType: String, CodableWithUnknown {
case A, B, C..
}
protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
where RawValue: Decodable, AllCases: BidirectionalCollection { }
extension CaseIterableDefaultsLast {
init(from decoder: Decoder) throws {
self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
}
}
enum SomeType: String, CaseIterableDefaultsLast {
case A, B, C, D
}
2.3 KeyedDecodingContainer 이용하기
protocol UnknownCaseCodable: Codable where Self: RawRepresentable, Self.RawValue: Codable { }
extension KeyedDecodingContainer {
func decodeIfPresent<T: UnknownCaseCodable>(_ type: T.Type, forKey key: K) throws -> T? {
try? decode(T.self, forKey: key)
}
}
enum ItemType: String, UnknownCaseCodable {
case A, B, C
}
개인적으로는 property-wrapper를 사용하는 방법과 마지막 2.3 방법을 고민하다가 마지막 방법을 이용했다. 기존 코드들에서도 Codable을 UnknownCaseCodable로 고쳐주기만 해서 간편했다.