[Swift] enum에 정의되지 않은 값으로 디코딩을 시도할 때 발생하는 문제

Decoding enum with invalid value

naljin
7 min readFeb 1, 2021

문제 상황

struct ResponseModel: Codable {
var title: String?
var pageType: PageType?
enum PageType: Int, Codable {
case zero = 0
}
}
//정의되지 않은 pageType 이 들어옴
let jsonString = "{\"title\":\"My Title\",\"pageType\":1}"
let result = try? JSONDecoder().decode(ResponseModel.self, from: jsonString.data(using: .utf8)!)

위와 같이 정의된 ResponseModel로 data를 디코딩을 한다고 해보자. 이때 들어오는 data 중 pageTypeResponseModel 에 정의되지 않은 값이다.

내 예상은 title 에는 정상값이 들어오고, pageType은 nil 이 들어오는 것이었다.

결과는?

print(result?.title ?? "nil") //nil
print(result?.pageType ?? "nil") //nil

둘다 nil 이 찍혀버린다 ㅎ;

해결 방법

init(from decoder: Decoder) throws 를 이용한 커스텀 디코딩이 필요하다.

1. enum 안에 init(from:) 구현

enum PageType: Int, Codable {
case zero = 0
case unknown
init(from decoder: Decoder) throws {
self = try PageType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}

위와 같이 case unknown 을 추가로 선언해두고, 정의된 RawValue 와 맞지 않으면 .unknown 으로 매핑시켜준다.

print(result?.title ?? "nil") //My Title
print(result?.pageType ?? "nil") //.unknown

2. Model 안에 init(from:) 구현

1번 방법의 경우 커스텀 디코딩을 구현할때 self 에 optional 값을 할당할 수 없기 때문에 무조건 unknown 등으로 매핑되게 해야한다. 따라서 pageType 이 nil 이 될 경우가 없다.

정의되지 않은 enum 값이 들어왔을때 pageType 값이 nil 이 되게 하려면, ResponseModel 에서 커스텀 디코딩을 구현한다. 이곳에서는 pageType 에 optional 값을 할당할 수 있기 때문이다.

struct ResponseModel: Codable {  var title: String?
var pageType: PageType?
enum PageType: Int, Codable {
case zero = 0
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
title = try? values.decode(String.self, forKey: .title)
pageType = try? values.decode(PageType.self, forKey: .pageType)
}
}

다만 보다시피 ResponseModel에 선언된 다른 값들도 일일이 디코딩이 필요하다는 단점이 있다. 어쨌든 결과는 아래와 같이 찍힌다.

print(result?.title ?? "nil") //My Title
print(result?.pageType ?? "nil") //nil

이외에 추가적인 다른 방법들이 궁금하면 아래 사이트들을 참고하자.

추가 배경 지식 — json decode와 에러 핸들링 (feat. try)

기본적으로 iOS에서는 json 을 디코딩 하기 위해 아래와 같이 JSONDecoderdecode 함수를 이용한다.

//Call can throw, but it is not marked with 'try' and the error is not handled
let result = JSONDecoder().decode(ResponseModel.self, from: data)

이때 decode 함수는 throw 를 던질 수 있기 에러 핸들링이 필요하다.

1. try catch

기본적으로 try catch 를 통해 에러 핸들링을 할 수 있다.

do {
let result = try JSONDecoder().decode(ResponseModel.self, from: data)
} catch {
print(error)
}

정의되지 않은 enum 값이 들어왔기 때는 다음과 같은 error가 프린트 된다.

dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "pageType", intValue: nil)], debugDescription: "Cannot initialize PageType from invalid Int value 1", underlyingError: nil))

2. try?

try catch 이외에도, Swift에서는 try? 의 사용을 통해 catch의 상황에서 변수에 nil을 할당하는 방법을 제공한다. (위의 예시에서 쓴 방법)

let result = try? JSONDecoder().decode(ResponseModel.self, from: data)print(result?.title ?? "nil") //nil
print(result?.pageType ?? "nil") //nil

이때 title 값은 json에서 제대로 들어왔음에도 불구하고, nil이 프린트 되는 것을 확인할 수 있다. 즉 정의되지 않은 enum 값이 들어오면 나머지 데이터도 디코딩하지 못하는 상황인데, 만약 제대로 들어온 title 값만 받기 위해서는 별도의 위에서 설명한 디코딩 처리가 필요하다.

3. try!

또한 try! 와 같이 무조건 유효한 값이 들어옴을 가정하고 변수에 값을 할당할 수도 있는데, 이때 정의되지 않은 enum 값이 들어오면 크래시가 발생한다.

let result = try! JSONDecoder().decode(ResponseModel.self, from: data)

결론적으로 Swift 에서는 json을 디코딩하는 다양한 방법이 존재하고, 이에 따라 정의되지 않은 enum 값이 들어올때 아래와 같은 상황들이 발생할 수 있다.

  • 전체 결과 빈 값으로 들어옴 (try?)
  • 앱 크래시 (try!)
  • 별도 에러 처리 (try catch)

참고

--

--