전편 JSON Encode, Decode (1) 기본 에서 기본적인 encode와 decode를 해보았다. 이번에는 조금 더 현실적인 예제를 통해서 심화된 내용을 다뤄보려고 한다.
이전에서 사용하던 Place
라는 struct를 다시 가져와보자:
struct Place: Identifiable, Codable {
let id = UUID()
let name: String
let latitude: Double
let longitude: Double
}
Codable
protocol을 준수하는 Codable
type은 CodingKeys
라는 특별한 enumeration을 정의할 수 있다.
CodingKey
enumeration이 존재하면, 이것은 codable type이 encode되거나 decode 될 때 반드시 포함되는 신뢰할 수 있는 property list가 된다. Enumeration case들의 이름은 상응하는 type의 property이다.
만약 instance를 decoding 할 때 특정 property가 존재하지 않거나, encode 할 시에 특정 property가 존재해서는 안된다면, CodingKey
enumeration에서 그것을 제외해야 한다. CodingKeys
에서 생략된 property는 Decodable이나 Codable의 automatic conformance를 위해 default value를 가져야 한다.
만약 serialized data format에서 사용하는 key와 너가 만든 data type의 property 이름이 일치하지 않는다면, CodingKeys
enumeration의 raw-value type을 String
으로 지정하고 alternative key를 제공해준다. 각 enumeration case에 제공해주는 raw value는 encoding과 decoding에 사용되는 key name이 된다. Case name과 raw value 간의 association은 modeling 하는 serialization format과 이름, capitalization 등과 일치하지 않아도 되게 하여 data structure의 이름을 Swift API Design Guidelines를 준수할 수 있게 해준다.
아래의 코드는 실제 서버로 부터 오는 id
라고 오는 property를 structure의 serverId
로 저장하는 예시이다:
import Foundation
struct Place: Identifiable, Codable {
let id = UUID()
let serverId: Int
let name: String
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey {
case serverId = "id"
case name
case latitude
case longitude
}
}
let p1 = Place(serverId: 10, name: "home", latitude: 37, longitude: 127)
print(p1)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
if let data = try? encoder.encode(p1), let jsonString = String(data: data, encoding: .utf8) {
print(jsonString)
}
코드를 실행해본 결과:
p1을 직접 출력했을 때는 id라는 UUID가 출력이 되었고, 서버에서 온 10이라는 id는 serverId
라는 property에 저장이 되었다. 하지만 encode 한 결과를 출력시키면, serverId
property가 id
라는 key로 나오는 것을 확인 할 수 있다.
만약 Swift type의 structure가 그것의 encode된 form과 다르다면, 너만의 encoding, decoding logic을 정의하기 위해 Encodable
과 Decodable
에 대한 custom implementation을 제공할 수 있다.
만약 latitude
, longitude
property가 이제부터 서버에서 coodrinate
라는 배열로 오는 경우를 생각해보자.
latitude
, longitude
를 많이 사용하였고coordinate[0]
, coordinate[1]
과 같이 코드에서 사용하는 것은 명료하지 않으므로 계속하여 longitude
, latitude
를 사용하는 것이 합리적인 선택일수 있다.
그렇다면, coordinate
배열로 들어온 값을 decoding 하여 latitude
, longitude
property에 넣어주고 이것을 서버로 전송할 때 다시 coordinate
배열로 encoding하는 로직을 구성해보려고 한다:
struct Place: Identifiable, Codable {
let id = UUID()
let serverId: Int
let name: String
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey {
case serverId = "id"
case name
case coordinate
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
serverId = try values.decode(Int.self, forKey: .serverId)
name = try values.decode(String.self, forKey: .name)
var coordinateContainer = try values.nestedUnkeyedContainer(forKey: .coordinate)
latitude = try coordinateContainer.decode(Double.self)
longitude = try coordinateContainer.decode(Double.self)
}
// encode...
}
CodingKeys
로 하는 container로 표현해준다(values
)serverId
와 name
값을 decode 하여 값을 넣어준다.coordinate
값을 nestedUnkeyedContainer
로 만들어준 다음 decode를 2번해주어 latitude
와 longitude
의 값을 넣어준다.decode가 제대로 되는지 확인해보자:
struct Place: Identifiable, Codable {
let id = UUID()
let serverId: Int
let name: String
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey {
case serverId = "id"
case name
case coordinate
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
serverId = try values.decode(Int.self, forKey: .serverId)
name = try values.decode(String.self, forKey: .name)
var coordinateContainer = try values.nestedUnkeyedContainer(forKey: .coordinate)
latitude = try coordinateContainer.decode(Double.self)
longitude = try coordinateContainer.decode(Double.self)
}
// encode...
}
// decode
let data = """
{
"id": 10,
"name": "my place",
"coordinate": [37.487307, 126.912969]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let decodeJson = try? decoder.decode(Place.self, from: data) {
print(decodeJson)
}
struct Place: Identifiable, Codable {
let id = UUID()
let serverId: Int
let name: String
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey {
case serverId = "id"
case name
case coordinate
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(serverId, forKey: .serverId)
var coordinateContainer = container.nestedUnkeyedContainer(forKey: .coordinate)
try coordinateContainer.encode(latitude)
try coordinateContainer.encode(longitude)
}
}
CodingKeys
를 key로 하는 container를 생성해준다.name
과 serverId
를 container에 넣어준다.coordinateContainer
라는 nestedUnkeyedContainer
를 만들어준다. 그 다음 latitude, longitude 순으로 encode 하여 값을 넣어준다. coordinateContainer
에 값을 채우면 1번에서 만든 container의 coordinate
라는 key에 들어가게 된다.