최근 MVP 아키텍처를 연습하는 토이 프로젝트를 진행하며 아래와 같이 Codable을 따르는 struct를 만들었다.
struct Articles: Codable {
let item: [Article]
struct Article: Codable {
let title: String
let originallink: String
let description: String
let pubDate: Date
}
}
Codable을 따르기만 하면 객체 type이 기본형이 아니더라도 item:[Article]
을 문제없이 decode할 수 있다. 그리고 Naver 검색 API를 사용하기 때문에 어떤 json이 넘어올지 이미 알고 있어서 property의 이름을 미리 response key 값과 동일하게 맞춰두었다. 그래서 CodingKey 프로토콜을 따르는 enum을 따로 선언하지 않았다.
지금 이 상태만으로도 (뭔가 허전해보이지만) Response를 decode하여 내 마음대로 휘뚜루마뚜루 사용하는 데에 문제가 전혀 없다. 하지만 문득 그런 생각이 든 것이다. '내가 만약에 Response key값을 미리 모른다면 그 값을 받아서 확인해볼 수가 없겠다. 하지만 그 값을 알고 싶으면 어떡해?'
//response로 넘어온 json string의 상태
let json = """
{
a: "aaa",
b: "bbb",
c: "ccc"
}
""".data(using: .utf8)!
//1번 Codable struct
struct Test1: Codable {
let a: String
let b: String
}
// 2번 Codable struct
struct Test2: Codable {
let a: String
let b: String
let c: String
let d: String?
}
let decoder = JSONDecoder()
let result1 = try decoder.decode(Test1.self, from:json)
let result2 = try decoder.decode(Test2.self, from:json)
/*
Test1(a:"aaa",b:"bbb")
Test2(a:"aaa",b:"bbb",c:"ccc",d:nil)
*/
1) 그런데 내가 c도 놓치지 싶지 않다면 어떻게 해야하는거지? 🙃 갖고싶다 c..
2) nil이 아니라 default값을 넣어줄 수는 없을까? nil 꼴 보기 싫어..🤬
공식 문서에 소개된 기본 코드를 약간 변형하여 예시를 만들었다. 먼저 전체 코드를 공유한다.
public init(from decoder:Decoder) throws
함수가 핵심이다.import Foundation
let json = """
[
{
"name": "Banana",
"point": 200,
"description": "A banana grown in Ecuador.",
"key1": "111",
"key2": "222"
},
{
"name": "Orange",
"point": 100
}
]
""".data(using: .utf8)!
struct Product: Decodable {
var name: String
var point: Int
var description: String?
var etc: [String:Any] = [:]
enum CodingKeys: String, CaseIterable, CodingKey {
case name
case point
case description
}
struct CustomCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
public init(from decoder: Decoder) throws {
let defaultContainer = try decoder.container(keyedBy: CodingKeys.self)
let extraContainer = try decoder.container(keyedBy: CustomCodingKeys.self)
// 내가 아는 키
self.name = try defaultContainer.decode(String.self, forKey: .name)
self.point = try defaultContainer.decode(Int.self, forKey: .point)
self.description = try defaultContainer.decodeIfPresent(String.self, forKey: .description)
for key in extraContainer.allKeys {
if CodingKeys.allCases.filter({ $0.rawValue == key.stringValue}).isEmpty {
// 내가 모르는 키가 나오면
let value = try extraContainer.decode(String.self, forKey: CustomCodingKeys(stringValue: key.stringValue)!)
self.etc[key.stringValue] = value
}
}
}
}
init?(stringValue: String)
을 통해 객체를 생성한다. (json의 key는 무조건 String이기 때문에 intValue는 return nil을 해도 무방하다.)struct CustomCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
for key in extraContainer.allKeys {
print(key)
}
/*
결과값
CustomCodingKeys(stringValue: "key1", intValue: nil)
CustomCodingKeys(stringValue: "description", intValue: nil)
CustomCodingKeys(stringValue: "key2", intValue: nil)
CustomCodingKeys(stringValue: "name", intValue: nil)
CustomCodingKeys(stringValue: "point", intValue: nil)
CustomCodingKeys(stringValue: "point", intValue: nil)
CustomCodingKeys(stringValue: "name", intValue: nil)
*/
if CodingKeys.allCases.filter({ $0.rawValue == key.stringValue}).isEmpty {
// 내가 모르는 키가 나오면
let value = try extraContainer.decode(String.self, forKey: CustomCodingKeys(stringValue: key.stringValue)!)
self.etc[key.stringValue] = value
}
Product(name: "Banana", point: 200, description: Optional("A banana grown in Ecuador."), etc: ["key2": "222", "key1": "111"])
Product(name: "Orange", point: 100, description: nil, etc: [:])
위의 코드에서는 var description
이 optional 타입이고 decode를 할 때에도 decodeIfPresent()
함수를 사용한다. response 안에 해당 값이 존재하지 않을 수 있기 때문이다. 값이 존재하지 않을 경우 nil로 바인딩 해주기 위해서이다.
struct Product: Decodable {
// ...
var description: String?
var etc: [String:Any] = [:]
public init(from decoder: Decoder) throws {
//...
self.description = try defaultContainer.decodeIfPresent(String.self, forKey: .description)
}
하지만 애초에 타입별로 기본 값을 정해줄 수 있다면 nil을 걱정하지 않아도 된다. 이는 위의 dynamic key를 상대하는 것보다 훨씬 간단하다. try?
를 통해 nil이 걸리면 default값을 사용하도록 ??
로 처리한다.
struct Product: Decodable {
// ...
var description: String
var etc: [String:Any] = [:]
public init(from decoder: Decoder) throws {
//...
self.description = (try? defaultContainer.decode(String.self, forKey: .description)) ?? "default description"
/*
결과값
Product(name: "Banana", point: 200, description: "A banana grown in Ecuador.", etc: ["key2": "222", "key1": "111"])
Product(name: "Orange", point: 100, description: "default description", etc: [:])
*/
}
참고자료 😇 친절한 애플 공식 문서
https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types
개인적으로 Codable을 사용하다가 열받는 지점이 몇 가지 있었는데 그 중 한 가지를 정리해보았다. 다음에는 Depth가 다른 json 값을 merge하기 위해 nestedContainer
를 사용하는 방법을 정리해보려 한다.