JSON을 활용한 커스텀 타입

Swift에서 JSON형식의 데이터를 다루기위해 encoding과 decoding을 사용할때

우리는 JSONDecoder와 JSONEncoder클래스로부터 제공되는 형식을 사용해야만합니다.

그렇다면 Encoding과 Decoding은 무엇일까요?

데이터를 다루는 모델을 Swift코드로 작성할 때

Codable 프로토콜을 따르는 타입을 작성하여 JSON 데이터와 그 데이터를 Swift에서 표현하는 방법간에 변환법을 알아보겠습니다.



배열로부터 데이터읽기

만약 JSON데이터의 요소들이 동일한 타입의 배열들일때 배열의 타입은 Codable 프로토콜을 따라야합니다.

전체 배열을 decode하거나 encode할때는 [Element].self를 사용해야합니다.

아래의 예시에 있는 배열은 Codable 프로토콜을 따르므로 자동으로 decodable합니다.

import Foundation

let json = """
[
    {
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "name": "Orange",
        "points": 100
    }
]
""".data(using: .utf8)!

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

print("The following products are available:")
for product in products {
    print("\t\(product.name) (\(product.points) points)")
    if let description = product.description {
        print("\t\t\(description)")
    }
}



Key 이름 변경

우리가 작성한 JSON을 활용하기위한 Swift모델의 데이터이름(Key값)과 실제 JSON데이터의 Key값이 다를수도 있습니다.

Codable, Decodable, Encodable을 따르는 타입안에 CodingKey를 따르는 열거형을 선언해줌으로써 JSON파일의 데이터 Key값과

Swift코드에서 다르게 쓰이는 동일한 데이터의 Key값을 매핑시켜줄 수 있습니다.

아래의 코드에서는 JSON의 “product_name” 은 name으로 “product_cost”는 points로 사용할 수 있는 코드입니다.

import Foundation

let json = """
[
    {
        "product_name": "Bananas",
        "product_cost": 200,
        "description": "A banana grown in Ecuador."
    },
    {
        "product_name": "Oranges",
        "product_cost": 100,
        "description": "A juicy orange."
    }
]
""".data(using: .utf8)!

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
    
    private enum CodingKeys: String, CodingKey {
        case name = "product_name"
        case points = "product_cost"
        case description
    }
}

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

print("The following products are available:")
for product in products {
    print("\t\(product.name) (\(product.points) points)")
    if let description = product.description {
        print("\t\t\(description)")
    }
}

“description”의 경우 양쪽에서 모두 같은 이름으로 쓰이지만 GroceryProduct의 요소이므로 열거형안에도 명시를 해줍니다.

또한 양쪽에서 같은 이름으로 사용하므로 위의 name이나 points와 달리 raw value를 지정해주지 않아도 괜찮습니다.



Nested Data 접근

앱을 설계할때 우리는 JSON데이터를 외부에서 가져와 사용할 수도 있고 로컬에 있는 JSON데이터를 사용할 수도 있습니다.

이런 두 경우에서 모두 우리가 작성한 JSON데이터를 사용하기위한 모델과 실제 JSON데이터간의 차이는 존재할 수 있습니다.

떄떄로 Swift에서 사용하는 논리적 데이터의 묶음은 여러개의 객체나 배열로 JSON데이터에 군집화되어 있습니다.

이런 구조적인 차이를 decodable타입을 매개체로 활용하여 안전하게 decode시킬 수 있습니다.

외부에서 전달되는 JSON 데이터를 이런 일련의 과정들을 initializer로 활용하여 우리의 앱에 사용할 수 있게됩니다.

아래의 코드는 grocery store과 물품의 목록을 나타내는 코드입니다.

import Foundation

struct GroceryStore {
    var name: String
    var products: [Product]
    
    struct Product: Codable {
        var name: String
        var points: Int
        var description: String?
    }
}

아래의 코드에 작성되어있는 JSON데이터의 정보를 JSON데이터 활용 API는 담을 수 있어야합니다!

let json = """
[
    {
        "name": "Home Town Market",
        "aisles": [
            {
                "name": "Produce",
                "shelves": [
                    {
                        "name": "Discount Produce",
                        "product": {
                            "name": "Banana",
                            "points": 200,
                            "description": "A banana that's perfectly ripe."
                        }
                    }
                ]
            }
        ]
    },
    {
        "name": "Big City Market",
        "aisles": [
            {
                "name": "Sale Aisle",
                "shelves": [
                    {
                        "name": "Seasonal Sale",
                        "product": {
                            "name": "Chestnuts",
                            "points": 700,
                            "description": "Chestnuts that were roasted over an open fire."
                        }
                    },
                    {
                        "name": "Last Season's Clearance",
                        "product": {
                            "name": "Pumpkin Seeds",
                            "points": 400,
                            "description": "Seeds harvested from a pumpkin."
                        }
                    }
                ]
            }
        ]
    }
]
""".data(using: .utf8)!

API에서 반환된 JSON데이터에는 Swift 타입에 명시되어있는 것보다 더 많은 정보를 담고있습니다.