JSON Encode, Decode (2) CodingKey

woo94·2023년 12월 24일
0

swift

목록 보기
4/5

Intro

전편 JSON Encode, Decode (1) 기본 에서 기본적인 encode와 decode를 해보았다. 이번에는 조금 더 현실적인 예제를 통해서 심화된 내용을 다뤄보려고 한다.

이전에서 사용하던 Place 라는 struct를 다시 가져와보자:

struct Place: Identifiable, Codable {
    let id = UUID()
    let name: String
    let latitude: Double
    let longitude: Double
}

CodingKey

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로 나오는 것을 확인 할 수 있다.

Manual Encode, Decode

만약 Swift type의 structure가 그것의 encode된 form과 다르다면, 너만의 encoding, decoding logic을 정의하기 위해 EncodableDecodable에 대한 custom implementation을 제공할 수 있다.

만약 latitude, longitude property가 이제부터 서버에서 coodrinate 라는 배열로 오는 경우를 생각해보자.

  1. 이미 앱의 많은 부분에서 latitude, longitude를 많이 사용하였고
  2. coordinate[0], coordinate[1] 과 같이 코드에서 사용하는 것은 명료하지 않으므로

계속하여 longitude, latitude를 사용하는 것이 합리적인 선택일수 있다.

그렇다면, coordinate 배열로 들어온 값을 decoding 하여 latitude, longitude property에 넣어주고 이것을 서버로 전송할 때 다시 coordinate 배열로 encoding하는 로직을 구성해보려고 한다:

Decoding logic

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...
}
  1. 이 decoder에 저장된 data를 key type을 CodingKeys로 하는 container로 표현해준다(values)
  2. serverIdname 값을 decode 하여 값을 넣어준다.
  3. coordinate 값을 nestedUnkeyedContainer로 만들어준 다음 decode를 2번해주어 latitudelongitude의 값을 넣어준다.

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)
}

Encoding logic

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)
	}
}
  1. CodingKeys를 key로 하는 container를 생성해준다.
  2. nameserverId를 container에 넣어준다.
  3. coordinateContainer라는 nestedUnkeyedContainer를 만들어준다. 그 다음 latitude, longitude 순으로 encode 하여 값을 넣어준다. coordinateContainer에 값을 채우면 1번에서 만든 container의 coordinate라는 key에 들어가게 된다.

참고

https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

profile
SwiftUI, node.js와 지독하게 엮인 사이입니다.

0개의 댓글

관련 채용 정보