Swift - Response Decoding: 괴상하고 다양한 양식들 디코딩

4 분 소요


외주 작업할 때 처리해야 했던 재밌고 다양한 디코딩들 정리
파라미터 이름 바꾸기나 4번은 흔한 편이지만, 혹시 2, 3번 때문에 곤란한 분들 있을까봐 정리함니다…

  1. 파라미터 이름 바꾸기
  2. 있을 수도 없을 수도 있는 파라미터
  3. 이름이 바뀔 수 있는 파라미터
  4. 타입이 다를 수 있는 파라미터


Best 상황

struct Post: Decodable {
    var title: String
    var contents: String
}

간단하게 이런 모델이 있다고 봅시다~

{
    "title": "정상적인 타이틀",
    "contents": "정상적인 내용"
}

서버에서 온 데이터는 이런 식으로 오면 아무런 문제가 없다
최고의 상황이며 아마 대부분 이렇다.


서버에서 온 파라미터 이름이 마음에 안 들어

{
    "jemock": "이름이 이상한 타이틀",
    "naeyong": "이름이 이상한 내용"
}

근데 서버에서 온 데이터가 이러면 어떡할까요??
파라미터 이름이 달라서 디코딩이 실패함니다
그렇다고 저런 파라미터 쓰고 싶진 않은데?? 하면 어떡할까??

방법 1. 모델 하나 더 추가하기

// 일단 얘로 디코딩하고
struct Post2: Decodable {
    var jemock: String
    var naeyong: String
}

// 다시 맵핑
let post = post2.map {
        Post(title: $0.jemock,
             contents: $0.naeyong)
    }

펀하고 쿨하지 않은 방법으로는 이렇게 그냥 모델을 서버 용으로 다시 만들고 기존 모델에 매핑하기가 있다

방법 2. CodingKeys 사용

struct Post: Decodable {
    var title: String
    var contents: String?
    
    enum CodingKeys: String, CodingKey {
        case title = "jemock"
        case contents = "naeyong"
    }
}

하지만 새로 모델 추가할 필요 없이, 이렇게 코딩키를 지정해주면 간단히 알아서 저 키를 사용해서 디코딩해준다.

+

{
    "title": "정상적인 타이틀",
    "naeyong": "이름이 이상한 내용"
}

어… 근데 일부만 이상하다면??

enum CodingKeys: String, CodingKey {
    case title
    case contents = "naeyong"
}

당연히 이렇게 일부만 지정하면 됨


있을 수도 없을 수도 있는 파라미터

{
    "title": "정상적인 타이틀",
    "contents": "정상적인 내용"
},
{
    "title": "정상적인 타이틀"
}

그런데 놀랍게도… contents가 있을 수도 없을 수도 있는 상황이라면 어떡할까?

방법 1. 서버에 바꿔달라 하기

{
    "title": "정상적인 타이틀",
    "contents": null
}
struct Post: Decodable {
    var title: String
    var contents: String?
}

없을 때는 null로 보내주세요 하면 제일 쉽게 해결 됨. 코딩키도 필요없다

방법 2. decodeIfPresent 사용하기

struct Post: Decodable {
    var title: String
    var contents: String?
    
    enum CodingKeys: String, CodingKey {
        case title, contents
    }
}
struct Post: Decodable {
    init(from decoder: Decoder) throws {
        // 1
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // 2
        self.title = try container.decode(String.self, forKey: .title)
        
        // 3
        self.contents = try container.decodeIfPresent(String.self, forKey: .contents)
    }
}

이쯤 되면 이제 셀프 디코딩을 해야 한다(코딩키 필요함).
디코더로 init하는 과정을 내가 정의하고 직접 디코딩하면 됨

  1. 일단 코딩키들로 컨테이너를 생성한다.
  2. 멀쩡한 녀석들은 그냥 키에 따라 decode()해 주면 됨.
  3. 이제 문제가 되는 애는 decodeIfPresent를 사용한다. decodeIfPresent는 말 그대로 있으면 함 해봐라~ 라서 Optional로 반환해줌


이름이 바뀔 수 있는 파라미터

{
    "title": "정상적인 타이틀",
    "contents": "정상적인 내용"
},
{
    "title": "정상적인 타이틀",
    "naeyong": "이름이 이상한 내용"
}

오~~ 이제 좀 된 줄 알았더니… 정말 신기하게도… 이렇게 두 개가 섞여서 온다면 어떻게 될까요??
contentsnaeyong이 같은 것을 나타내는데 파라미터 이름이 다르다면?? 뭐로 올 지 모르겠는데? 싶을 때
물론 보통의 경우엔 이런 일이 있을 리가 없지만… 정말 신기하게도 이런 일이 있다면

struct Post: Decodable {
    var title: String
    var contents: String = ""
    
    enum CodingKeys: String, CodingKey {
        case title

        case contents
        case naeyong // New!
    }
}

이렇게 되면 코딩키를 더 추가해줘야 한다.

struct Post: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.title = try container.decode(String.self, forKey: .title)
        
        // Changed
        if let decodedContents = try container.decodeIfPresent(String.self, forKey: .contents) {
            self.contents = decodedContents
        } else if let decodedContents = try container.decodeIfPresent(String.self, forKey: .naeyong) {
            self.contents = decodedContents
        }
    }
}

이제 간단하다
문제가 되는 애는 여러 번 디코딩을 시도하면 된다.
.contents 코딩키로 한 번 시도해 보고, 없으면 다시 .naeyong 코딩키로 시도해보면 된다. 물론 두 번 다 옵셔널이기 때문에, String 타입을 사용하고 싶다면 초기값을 세팅해줘야 함.


타입이 다를 수 있는 파라미터

{
    "title": "정상적인 타이틀",
    "contents": "정상적인 내용"
},
{
    "title": "정상적인 타이틀",
    "contents": ["형식이 갑자기", "어레이로 오는 내용"]
}

오!! 놀랍게도!! 일케 오면 어떡할까
contents가 String으로 오기도 하고 Array로 오기도 한다면??

방법 1. try-catch

struct Post: Decodable {
    var title: String
    var contents: String = ""
    
    enum CodingKeys: String, CodingKey {
        case title, contents
    }
}
struct Post: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.title = try container.decode(String.self, forKey: .title)
        
        // Changed
        do {
            self.contents = try container.decode(String.self, forKey: .contents)
        } catch {
            self.contents = try container.decode([String].self, forKey: .contents).joined(separator: "\n")
        }
    }
}

우선 String으로 한 번 시도해 보고, 안 되면 다른 타입으로도 시도해 보면 된다.

방법 2. propertyWrapper

근데 저게 여기저기 다 저러면 어떡할까??

struct Model1: Decodable {
    var title: String
    var isConfirm: Bool
}

struct Model2: Decodable {
    var title: String
    var isExpired: Bool
}

Node js 서버를 써야 했던 일이 있었는데… 타입이 이상하게 들어가서 Bool 값을 원했으나 실제로 true로 들어가있기도 하고 "true"와 같이 문자열로 들어가 있기도 한 괴랄한 상황이 된 적이 있었다
근데 저런 Bool 값이 여기저기 다 쓰였다. 모든 모델들 하나하나 init() 만들어 주기는 너무 귀찮은데?? 싶을 때…

@propertyWrapper
struct BoolWrapper {
    let wrappedValue: Bool
}

extension BoolWrapper: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        var decodedValue: Bool?
        do {
            let stringValue = try container.decode(String.self)
            decodedValue = stringValue == "true"
        } catch {
            decodedValue = try container.decode(Bool.self)
        }
        wrappedValue = decodedValue ?? false
    }
}

propertyWrapper를 사용하면 된다.
위에서처럼 똑같이 직접 디코딩해주는 건 다를 게 없는데,

struct Model1: Decodable {
    var title: String
    @BoolWrapper var isConfirm: Bool
}

struct Model2: Decodable {
    var title: String
    @BoolWrapper var isExpired: Bool
}

실제 모델에서는 그냥 이렇게 붙여서 사용해주면 돼서 모델들 마다 init()을 추가할 필요가 없다!!
+ 코딩키도 추가할 필요가 없다~~ 왜냐하면 해당 파라미터가 해당 이름으로 존재하기는 하는 경우기 때문에 명시해줄 필요가 없음



굿


태그: ,

카테고리:

업데이트:

댓글남기기