API를 통해 JSON 응답을 받을 때 만약 여러 데이터 타입을 가진 키가 있다면 디코딩이 쉽지 않다. 자주는 아니지만 불가능한 경우도 아닌데, 예를 들면 결과가 다음과 같다.
[
{
id: 0,
title: zero,
price: 19000,
value: “Hello”
},
{
id: 1,
title: one,
price: 9900,
value: false
},
{
id: 2,
title: two,
price: 1000,
value: 1
},
{
id: 3,
title: three,
price: 23000,
value: [“Hello”, “World”]
},
]
value
라는 key가 String, Bool, Int 등 다양한 타입을 결과로 가지고 있다. 이럴 때 단순히 Any
로 지정하고 싶지만 당연하게도 Any
는 Decodable하지 않다..🥲
해결 방법은 Decoder
를 받는 init
을 통해 수동(?)으로 decode를 구현하면 된다!
/// A type that can decode itself from an external representation.
public protocol Decodable {
/// Creates a new instance by decoding from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
init(from decoder: Decoder) throws
}
init(from decoder: Decoder)
의 경우 Decodable
프로토콜의 요소로 평상시에는 구현하지 않아도 되지만 직접 구현해서 사용할 수 있다.
struct Product: Decodable {
let id: Int
let title: String
let price: Int
let value: LandingValue
}
이렇게 모델 타입이 있다면 value
의 타입으로는 멀티 타입을 자체를 나타낼 타입을 선언 후 지정한다.
struct LandingValue: Decodable {
let stringValue: String?
let intValue: Int?
init(stringValue: String? = nil, intValue: Int? = nil) {
self.stringValue = stringValue
self.intValue = intValue
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// String으로 decode 시도
if let value = try? container.decode(String.self) {
self = .init(stringValue: value)
return
}
// Int로 decode 시도
if let value = try? container.decode(Int.self) {
self = .init(intValue: value)
return
}
throw DecodingError.typeMismatch(
LandingInfo.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Type is not matched",
underlyingError: nil)
)
}
}
각 타입에 대응되는 프로퍼티를 선언하면 되는데 만약 Int와 String이 될 수 있는 타입이라면 위와 같다.
decoder에서 Container를 가져오는데 단일 값이기 때문에 singleValueContainer
로 가져올 수 있고 또는 container(keyedBy:)
를 통해 가져올 수 있다.
container의 decode 메서드를 통해 각 타입으로 디코딩을 시도하면 된다. 만약 decode 된다면 해당 타입으로 초기화해주고, 안 된다면 다음 타입으로 넘어간다. 마지막까지 실패했다면 DecodingError
를 throw 하게 된다.
enum으로 구현 가능하다.
enum LandingValue: Decodable {
case stringValue(String)
case intValue(Int)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(String.self) {
self = .intValue(value)
return
}
if let value = try? container.decode(Int.self) {
self = .targetInt(value)
return
}
throw DecodingError.typeMismatch(
LandingInfo.self,
DecodingError.Context(codingPath: decoder.codingPath,
debugDescription: "Type is not matched",
underlyingError: nil)
)
}
var targetS: String {
switch self {
case .stringValue(let string):
return string
case .intValue(let int):
return "\(int)"
}
}
var targetI: Int {
switch self {
case .stringValue(_):
return 0
case .intValue(let int):
return int
}
}
}
decode
구문 역시 do-catch
문으로도 작성할 수 있으니 취향 대로 구현하면 된다 🙂
또한 Codable
이나 encoding을 지원하는 경우 동일한 방식으로 encode(to encoder: Encoder)
를 구현하면 된다!
참고 링크
Decoding JSON in Swift