Swift - Response Decoding: 괴상하고 다양한 양식들 디코딩
외주 작업할 때 처리해야 했던 재밌고 다양한 디코딩들 정리
파라미터 이름 바꾸기나 4번은 흔한 편이지만, 혹시 2, 3번 때문에 곤란한 분들 있을까봐 정리함니다…
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하는 과정을 내가 정의하고 직접 디코딩하면 됨
- 일단 코딩키들로 컨테이너를 생성한다.
- 멀쩡한 녀석들은 그냥 키에 따라
decode()
해 주면 됨. - 이제 문제가 되는 애는
decodeIfPresent
를 사용한다.decodeIfPresent
는 말 그대로 있으면 함 해봐라~ 라서 Optional로 반환해줌
이름이 바뀔 수 있는 파라미터
{
"title": "정상적인 타이틀",
"contents": "정상적인 내용"
},
{
"title": "정상적인 타이틀",
"naeyong": "이름이 이상한 내용"
}
오~~ 이제 좀 된 줄 알았더니… 정말 신기하게도… 이렇게 두 개가 섞여서 온다면 어떻게 될까요??
contents
와 naeyong
이 같은 것을 나타내는데 파라미터 이름이 다르다면?? 뭐로 올 지 모르겠는데? 싶을 때
물론 보통의 경우엔 이런 일이 있을 리가 없지만… 정말 신기하게도 이런 일이 있다면
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()
을 추가할 필요가 없다!!
+ 코딩키도 추가할 필요가 없다~~ 왜냐하면 해당 파라미터가 해당 이름으로 존재하기는 하는 경우기 때문에 명시해줄 필요가 없음
굿
댓글남기기