[Swift] Decoding JSON With Multiple Types Key

Judy·2023년 7월 19일
0

iOS

목록 보기
25/28
post-thumbnail

여러 타입을 가진 JSON Decoding

상황

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라는 keyString, Bool, Int 등 다양한 타입을 결과로 가지고 있다. 이럴 때 단순히 Any로 지정하고 싶지만 당연하게도 AnyDecodable하지 않다..🥲


해결 방법

해결 방법은 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 프로토콜의 요소로 평상시에는 구현하지 않아도 되지만 직접 구현해서 사용할 수 있다.

방법 1) struct로 구현하기

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)
        )
    }
}

각 타입에 대응되는 프로퍼티를 선언하면 되는데 만약 IntString이 될 수 있는 타입이라면 위와 같다.

decoder에서 Container를 가져오는데 단일 값이기 때문에 singleValueContainer로 가져올 수 있고 또는 container(keyedBy:)를 통해 가져올 수 있다.

container의 decode 메서드를 통해 각 타입으로 디코딩을 시도하면 된다. 만약 decode 된다면 해당 타입으로 초기화해주고, 안 된다면 다음 타입으로 넘어간다. 마지막까지 실패했다면 DecodingError를 throw 하게 된다.

방법 2) enum으로 구현

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

profile
iOS Developer

0개의 댓글