[Apple] Using JSON with Custom Types

J.Noma·2022년 1월 25일
0

iOS : Foundation 모음

목록 보기
2/3

Reference


🐸 예제

✔️ 배열 다루기

decode()의 parameter로 배열을 주면 인스턴스 배열이 만들어집니다

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 바꾸기

CodingKeys 열거형을 정의하여 코드에서 사용할 프로퍼티명이 Swift API Design Guidelines를 준수하도록 변경해줄 수 있습니다

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

✔️ Nested type 다루기

코드만 봐도 이해가능

struct GroceryStoreService: Decodable {
    let name: String
    let aisles: [Aisle]
    
    struct Aisle: Decodable {
        let name: String
        let shelves: [Shelf]
        
        struct Shelf: Decodable {
            let name: String
            let product: GroceryStore.Product
        }
    }
}

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

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

let stores = serviceStores.map { GroceryStore(from: $0) }

for store in stores {
    print("\(store.name) is selling:")
    for product in store.products {
        print("\t\(product.name) (\(product.points) points)")
        if let description = product.description {
            print("\t\t\(description)")
        }
    }
}

✔️ Depth가 다른 프로퍼티를 동일한 Depth로 병합할 수도 있다

아래 JSON에서 Bananapoints/description은 depth가 다르다

import Foundation

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

인코딩/디코딩 로직을 직접 구현함으로써
이들을 병합하여 동일한 Depth로 만들어줄 수도 있다

예시로, Banananame이라는 프로퍼티로 만들고
name/points/description 3개 프로퍼티를 가지는 Product 타입으로 디코딩해보자

JSONDecoder가 가진 container 등의 메서드를 추가로 활용한다

struct GroceryStore {
    struct Product {
        let name: String
        let points: Int
        let description: String?
    }

    var products: [Product]
}

extension GroceryStore: Decodable {
    struct ProductKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }

        static let points = ProductKey(stringValue: "points")!
        static let description = ProductKey(stringValue: "description")!
    }
    
    public init(from decoder: Decoder) throws {
        var products = [Product]()
        let container = try decoder.container(keyedBy: ProductKey.self)
        for key in container.allKeys {
            // Note how the `key` in the loop above is used immediately to access a nested container.
            let productContainer = try container.nestedContainer(keyedBy: ProductKey.self, forKey: key)
            let points = try productContainer.decode(Int.self, forKey: .points)
            let description = try productContainer.decodeIfPresent(String.self, forKey: .description)

            // The key is used again here and completes the collapse of the nesting that existed in the JSON representation.
            let product = Product(name: key.stringValue, points: points, description: description)
            products.append(product)
        }

        self.init(products: products)
    }
}

let decoder = JSONDecoder()
let decodedStore = try decoder.decode(GroceryStore.self, from: json)

print("The store is selling the following products:")
for product in decodedStore.products {
    print("\t\(product.name) (\(product.points) points)")
    if let description = product.description {
        print("\t\t\(description)")
    }
}


🐥 Encoding/Deconding 시 주의사항

Codable은 다형성을 살릴 수 없다

상속관계를 가지는 class에서 subclass의 인스턴스를 superclass로 타입 캐스팅하더라도 '겉모습은 superclass이면서 실체는 subclass를 유지하는' 다형성을 보입니다

하지만 아래와 같이 Codable 프로토콜로 Encoding/Decoding하는 경우에는 다형성을 가질 수 없는 문제가 있습니다

class AAA: Codable {
    let aaa: Int = 1
    var description: String {
        return "AAA"
    }
}

class BBB: AAA {
    let bbb: Int = 2
    override var description: String {
        return "BBB"
    }
}

class CCC: BBB {
    let ccc: Int = 3
    override var description: String {
        return "CCC"
    }
}

do{
    let encoded = try JSONEncoder().encode(CCC())
    let decoded = try JSONDecoder().decode(AAA.self, from: encoded)
    print(decoded.description)
    // print AAA (?!?)
    
} catch {
    print(error)
}

NSCoding 프로토콜을 사용하는 방법은 YodaCodd님 포스팅을 참고바랍니다

profile
노션으로 이사갑니다 https://tungsten-run-778.notion.site/Study-Archive-98e51c3793684d428070695d5722d1fe

0개의 댓글