THIS IS ELLIE

동일한 키 다른 타입 디코딩하기 본문

낑낑/Troubleshooting

동일한 키 다른 타입 디코딩하기

Ellie Kim 2021. 3. 30. 18:25

Landmark구조체가 있다고 가정해본다.

struct Landmark {
    var name: String
    var foundingYear: Int
}

Landmark에 Codable프로토콜을 채택한다.
Codable프로토콜(Decodable, Encodable)을 채택하면 따로 선언하지 않아도 Codable메서드인 init(from: ) 및 encode(to :)를 지원한다.

struct Landmark: Codable {
    var name: String
    var foundingYear: Int
    
    // Landmark now supports the Codable methods init(from:) and encode(to:), 
    // even though they aren't written as part of its declaration.
}

Landmark 구조체는 PropertyListEncoder 그리고 JSONEncoder 클래스를 사용해 인코딩할 수 있다.

만약에 foundingYear이 String으로 올때도올 때도 있고 Int로 올 때도 있다고 생각해보자.
(거의 그러지는 않겠지만,, 혹시나) (겪어봄)

특정 타입으로 선언했는데 내가 정의한 타입이 아닌 타입이 내려오는 경우가 발생하면, 타입이 맞지 않아 디코딩 실패가 될 것이다.
그럴때는 사용하는 방법으로 초기화해주는 부분에서 아래와 같이 decodeIfPresent로 Int가 되는지 확인해보고 안 되면 String으로 되는지 확인해보는 방법이 있다.
사실 타입을 바꾸는 것이 아니라 타입은 String으로 선언되어 있고 Int로 벗겨지면 Int를 String으로 타입을 변경시켜 foundingYear에 넣는 방법이다. (디코딩 실패를 피하기 위해서 사용한 방법)

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    do {
        if let value = try container.decodeIfPresent(Int.self, forKey: .foundingYear) {
            foundingYear = String(value)
        } else {
            foundingYear = ""
        }
    } catch {
        foundingYear = try container.decodeIfPresent(String.self, forKey: .foundingYear) ?? ""
    }
}

또 다른 방법은 Codable을 채택한 enum을 하나 만드는 것이다.

IntOrString으로 생성하고 Int가될수도 있고 String이 될 수도 있는 변수의 타입을 IntOrString으로 선언해준다.
위 랜드마크 같은 경우는 foundingYear를 IntOrString으로 타입을 선언해준다.

enum IntOrString: Codable {
    case int(Int)
    case string(String)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Int.self) {
            self = .int(x)
            return
        }
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for IntOrString"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .int(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        }
    }
    
    func getNumber() -> NSNumber {
        switch self {
        case .int(let int):
            return NSNumber(value: int)
        case .string(let str):
            if let int = Int(str) {
                return NSNumber(value: int)
            }
            return 0
        }
    }
    
    func getString() -> String {
        switch self {
        case .string(let string):
            return string
        case .int(let int):
            return String(int)
        }
    }
}

enum의 associated value를 활용했다.
이는 선언 시점이 아니라 새로운 enum을 생성할 때 값을 저장한다.
초기화시에 if let으로 Int로 디코딩되는지 보고 String으로 디코딩되는지 보고 안되면 Int나 String이 아니기 때문에 throw를 한다.
값을 사용할 때도 편리하게 사용할 수 있다.
enum에 원하는 타입으로 뽑아낼 수 있도록 함수를 생성해준다.
ex) getNumber, getString

resource:
stackoverflow.com/questions/47318737/how-do-i-handle-decoding-two-possible-types-for-one-key-in-swift
developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

반응형