[Swift] enum에 정의되지 않은 값으로 디코딩을 시도할 때 발생하는 문제
문제 상황
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 중 pageType
은 ResponseModel
에 정의되지 않은 값이다.
내 예상은 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 을 디코딩 하기 위해 아래와 같이 JSONDecoder
의 decode
함수를 이용한다.
//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)