safe decoding of enum

김가영·2022년 6월 4일
0

swift

목록 보기
1/3
post-custom-banner

Problem

Optional&Decodable 로 정의된 enum을 decoding할 때, case로 정의되지 않은 String이 들어있는 경우 에러가 나게 된다.

원래 Decodable에서는 init(from decoder: Decoder) 을 정의해줘야하는데, Codable property로 이루어진 타입인 경우에는 자동으로 해당 함수가 생성된다. 이 때, 프로퍼티가 optional인 경우에는 decodeIfPresent를, non-optional인 경우에는 decode 를 호출한다. decodeIfPresent는 단순히 해당 키의 존재유무(그리고 값의 null 유무)만 확인한 후 나머지 프로세스는 decode와 동일하기 때문에, 키가 존재하지만, decoding이 실패하는 경우는 잡아내지 못하는 것.

Solution

0. Decodable init 구현

가장 쉬운 해결책은 직접 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을 할당해줄 수는 없다.)

1. Property Wrapper

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를 사용할 수 있다.
  • Optional로 정의됐을 때, decoding에 실패했으면 nil로 정의된다. (default 값을 설정해줄 수도 있긴 하다.)
  • Codable외의 제약을 주지 않아서 Enum외의 타입에도 이용할 수 있다.
@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]?
}
  • 추가로 위처럼 Array에서도 원래 element 중 하나만 실패해도 런타임 에러가 발생하는 것을, 성공한 것들만 반환하도록 바꾸는 property wrapper를 추가할 수도 있다.
  • 이렇게 data model마다 필요한 behavior를 다르게 설정할 수 있으며, Enum 외의 타입에도 적용할 수 있어서 확장성이 좋다.
  • 단 data model마다 추가해줘야한다는 점이 좀 귀찮은 점이다.

2. protocol 이용하기

2.1 unknown case로 설정하기

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..
}
  • enum이 해당 프로토콜을 conform하게하여, decoding 에 실패할 경우 unknown case로 설정한다. 단, unknown case를 각 enum마다 직접 추가해줘야한다.
  • nil로 설정할 수는 없다.

2.2 첫번째(또는 마지막) case로 설정하기

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
}
  • 실패할 경우 마지막 case로 설정한다.
  • 역시 nil로 설정할 수는 없다.

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
}
  • 프로퍼티가 Optional로 정의되었을 때 호출되는 decodeIfPresent를 바꿔준다. 실패할 경우 nil을 리턴하도록 한다.
  • 자동으로, 해당 enum을 Optional로 이용할 때에만 적용이 되기 때문에 안전하다. non-optional일 때 정의되지 않은 case가 들어오면 원래처럼 에러가 발생한다.

개인적으로는 property-wrapper를 사용하는 방법과 마지막 2.3 방법을 고민하다가 마지막 방법을 이용했다. 기존 코드들에서도 Codable을 UnknownCaseCodable로 고쳐주기만 해서 간편했다.

profile
개발블로그
post-custom-banner

0개의 댓글