1. JSON Serialization πŸ‘©β€πŸ’»

Codable이 λ‚˜μ˜€κΈ° 이전 λ°©μ‹μœΌλ‘œ 직접 Serialization 을 ν•΄μ•Όν•œλ‹€.

struct CoinData: Decodable {
    
    let time: String
    let coin: String
    let currency: String
    let rate: Double
    
}

protocol CoinManagerDelegate {
    func didUpdateCoin(_ manager: CoinManager, coinData: CoinData)
    func didFailWithError(_ manager: CoinManager, error: Error)
}

struct CoinManager {
    
    var delegate: CoinManagerDelegate?
    
    func fetchData(with urlString: String) {
        // 1. Create a URL
        guard let url = URL(string: urlString) else { return }
        
        // 2. Create a URLSession
        var session = URLSession(configuration: .default)
        /* Optional, 'value-key' pair. */
        // session.setValue("apiKey", forHTTPHeaderField: "X-CoinAPI-Key")
        // session.addValue("apiKey", forHTTPHeaderField: "X-CoinAPI-Key")  // Same to 'setValue' method.
        
        // 3. Give the session a task
        let task: URLSessionDataTask = session.dataTask(with: url) { (data, response, error) in
            if let error {
                print("ERROR: \(error.localizedDescription)")
                return
            }
            
            guard let data else { return }
            guard let coinData = parseJson(data) else { return }
            delegate?.didUpdateCoin(self, coinData: coinData)
        }
        
        // 4. Start the task
        task.resume()
    }
    
    private func parseJson(_ data: Data) -> CoinData? {
        do {
            let coinDictionary = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
            let time: String = coinDictionary?["time"] as! String
            let coin: String = coinDictionary?["asset_id_base"] as! String
            let currency: String = coinDictionary?["asset_id_quote"] as! String
            let rate: Double = coinDictionary?["rate"] as! Double
            let coinData = CoinData(time: time, coin: coin, currency: currency, rate: rate)
            return coinData
        } catch {
            delegate?.didFailWithError(self, error: error)
            return nil
        }
    }
    
}

2. Codable πŸ‘©β€πŸ’»

  • DTO에 Codable ν”„λ‘œν† μ½œμ„ μ±„νƒμ‹œν‚΄μœΌλ‘œμ¨ 인코딩과 디코딩을 μ†μ‰½κ²Œ ν•  수 μžˆλ‹€.
  • Codable 은 Encodable κ³Ό Decodable 을 λͺ¨λ‘ μ±„νƒν•˜λŠ” alias 이닀.

URLSession을 μ‚¬μš©ν•΄ 데이터 톡신을 직접 μ½”λ”©ν•˜κ±°λ‚˜, Codable 객체λ₯Ό μ§€μ›ν•˜λŠ” λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ μ‚¬μš© κ°€λŠ₯ν•˜λ‹€.

struct CoinData: Decodable {
    
    let time: String
    let coin: String
    let currency: String
    let rate: Double
    
    enum CodingKeys: String, CodingKey {
        case time
        case coin = "asset_id_base"
        case currency = "asset_id_quote"
        case rate
    }
    
}

protocol CoinManagerDelegate {
    func didUpdateCoin(_ manager: CoinManager, coinData: CoinData)
    func didFailWithError(_ manager: CoinManager, error: Error)
}

struct CoinManager {
    
    var delegate: CoinManagerDelegate?
    
    func fetchData(with urlString: String) {
        // 1. Create a URL
        guard let url = URL(string: urlString) else { return }
        
        // 2. Create a URLSession & task & start
        /*
         μ»€μŠ€ν„°λ§ˆμ΄μ§•μ„ ν•„μš”λ‘œ ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ μ•„λž˜μ™€ 같이
         `URLSession.shared.dataTask(with:completionHandler:)`λ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.
         */
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error {
                print("ERROR: \(error.localizedDescription)")
                return
            }
            
            guard let data else { return }
            guard let coinData = parseJson(data) else { return }
            delegate?.didUpdateCoin(self, coinData: coinData)
        }.resume()
    }
    
    private func parseJson(_ data: Data) -> CoinData? {
        let decoder = JSONDecoder()
        do {
            let coinData = try decoder.decode(CoinData.self, from: data)
            return coinData
        } catch {
            delegate?.didFailWithError(self, error: error)
            return nil
        }
    }
    
}

참고둜 μœ„ μ½”λ“œλŠ” Decodable ν”„λ‘œν† μ½œμ„ μ±„νƒν•œ CoinData DTOλ₯Ό κ·ΈλŒ€λ‘œ Entity둜 μ‚¬μš©ν•˜κ³  μžˆλ‹€. λ§Œμ•½, DTO와 Entityλ₯Ό λ‚˜λˆ„μ–΄ μ‚¬μš©ν•  경우, Codable 은 DTO만 μ±„νƒν•˜λ©΄ λœλ‹€.


3. Libraries that does not support Codable πŸ‘©β€πŸ’»

Firebase 의 Realtime Database, Firestore와 같은 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” Codable 을 μ§€μ›ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 직접 Serialization 을 ν•΄μ€˜μ•Όν•œλ‹€. ν•˜μ§€λ§Œ μœ„ JSONSerialization μ—μ„œμ™€ 같이 Swift Data 객체둜 λ°”κΎΈκΈ° μœ„ν•΄ 일일히 코딩을 ν•˜λŠ” 것은 λΆˆν•„μš”ν•œ boilerplate code λ₯Ό μƒμ„±ν•œλ‹€.

λ”°λΌμ„œ, JSONSerialization을 JSONEncoder, JSONDecoderκ³Ό ν•¨κ»˜ μ‚¬μš©ν•˜λ©΄ Codable 을 μ§€μ›ν•˜μ§€ μ•ŠλŠ” λΌμ΄λΈŒλŸ¬λ¦¬μ—λ„ μ μš©ν•  수 μžˆλ‹€.

1. JSON Serialization Methods

  • jsonObject(with:options:): Returns a Foundation object from given JSON data.
  • data(withJSONObject:options:): Returns JSON data from a Foundation object.

μ—¬κΈ°μ„œ Foundation objectλŠ” NSArray, NSDictionary, NSNumber, NSDate, NSString, NSNull 을 μ˜λ―Έν•œλ‹€. 그리고 데이터 톡신을 ν•  λ•Œ κ°€μž₯ λ°”κΉ₯ μ»¨ν…Œμ΄λ„ˆλŠ” 항상 Key-Value ν˜•νƒœλ₯Ό ν•˜κ³  μžˆμœΌλ―€λ‘œ, κ°€μž₯ λ°”κΉ₯ ν˜•νƒœλŠ” [NSString: id] νƒ€μž…μ˜ NSDictionay 라 λ³Ό 수 μžˆλ‹€. 그리고 Objective-C Foundation 의 NSDictionay λŠ” Swift 의 Dictionay와 λΈŒλ¦Ώμ§€λ˜λ―€λ‘œ, [String: Any] νƒ€μž…μ˜ Dictionary 와 JSON μ‚¬μ΄μ˜ ν˜•λ³€ν™˜μ΄λΌ λ³Ό 수 μžˆλ‹€.

λ”°λΌμ„œ λ‹€μ‹œ μ •λ¦¬ν•˜λ©΄ λ‹€μŒκ³Ό κ°™λ‹€.

  • jsonObject(with:options:): JSON -> Dictionay
  • data(withJSONObject:options:): Dictionary -> JSON

2. JSON Encoder and Decoder Methods

JSONEncoder

Encodable ν”„λ‘œν† μ½œμ„ μ€€μˆ˜ν•˜λŠ” Structureλ₯Ό JSON으둜 λ³€ν™˜ν•œλ‹€.

JSONDecoder

JSON을 Decodable ν”„λ‘œν† μ½œμ„ μ€€μˆ˜ν•˜λŠ” Structure둜 λ³€ν™˜ν•œλ‹€.

이λ₯Ό μ •λ¦¬ν•˜λ©΄ λ‹€μŒκ³Ό κ°™λ‹€.

  • JSONSerialization.jsonObject(with:options:): JSON -> Dictionary
  • JSONSerialization.data(withJSONObject:options:): Dictionary -> JSON
  • JSONEncoder().encode(_:): Structure: Encodable -> JSON
  • JSONDecoder().decode(_:from:): JSON -> Structure: Decodable

3. Casting between Dictionaries, JSON, and Structures

Codable 을 μ§€μ›ν•˜μ§€ μ•ŠλŠ” Firebase 의 Realtime Database, Firestore 라이브러리의 경우 직접 JSONSerializationλ₯Ό ν•΄μ•Όν•œλ‹€. 이듀은 [String: Any] νƒ€μž…μ˜ Dictionayλ₯Ό μ‚¬μš©ν•΄ ν†΅μ‹ ν•˜λŠ”λ°, 쿼리 κ²°κ³Ό Response 의 λ°μ΄ν„°λŠ” Any λ˜λŠ” [String: Any] νƒ€μž…μ„ κ°–λŠ”λ‹€. λ”°λΌμ„œ μ§μ ‘μ μœΌλ‘œ Codable 을 μ‚¬μš©ν•  수 μ—†κΈ° λ•Œλ¬Έμ— λ‹€μŒκ³Ό 같은 λ³€ν™˜ 과정을 생각해볼 수 μžˆλ‹€.

Encoding

Structure -> JSON -> NSDictionary

Decoding

Dictionay -> JSON -> Structure (or Any -> NSDictionay -> JSON -> Structure)


μœ„ λ³€ν™˜ 과정에 ν•„μš”ν•œ λ©”μ„œλ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

Encoding

Structure -> JSON: JSONEncoder().encode(_:)
JSON -> NSDictionary: JSONSerialization.jsonObject(with:options:)

Decoding

Dictionay -> JSON: JSONSerialization.data(withJSONObject:options:)
JSON -> Structure: JSONDecoder().decode(_:from:)

4. Custom Encoding Methods for Firebase

앱을 κ°œλ°œν•˜λŠ”λ° μžˆμ–΄ 맀번 인코딩/λ””μ½”λ”© μ½”λ“œλ₯Ό μž‘μ„±ν•˜λŠ” 것은 맀우 λΉ„νš¨μœ¨μ μ΄λ‹€. ν•΄λ‹Ή 앱에 잘 λ§žλ„λ‘ λ³„λ„μ˜ μœ ν‹Έμ„ λ§Œλ“€μ–΄ μ‚¬μš©ν•˜λŠ” 것이 μ’‹λ‹€.

func encode<T>(_ value: T) throws -> Data where T : Encodable
class func jsonObject(
    with data: Data,
    options opt: JSONSerialization.ReadingOptions = []
) throws -> Any

λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ T: Encodable을 λ°›μ•„ Anyλ₯Ό λ°˜ν™˜ν•˜λ©΄ λœλ‹€. μ΄λ•Œ ν•¨μˆ˜λŠ” λ‹€μ–‘ν•œ λ°©μ‹μœΌλ‘œ λ§Œλ“€ 수 μžˆλ‹€.

μ‹€μ œ μ½”λ“œλ₯Ό μ‚¬μš©ν•  λ•Œ λ³„λ„μ˜ μ—λŸ¬ 처리λ₯Ό 직접 ν•˜κΈ°λ₯Ό μ›ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ μ•„λž˜μ™€ 같이 μ—λŸ¬λ₯Ό throws ν•˜μ§€ μ•Šκ³  μ μ ˆν•œ 둜그λ₯Ό λ‚¨κΈ°κ±°λ‚˜ νŠΉμ • μ•Œλ¦Όμ°½μ„ λ„μš°λŠ” λ“±μ˜ κ³΅ν†΅λœ λ‘œμ§μ„ μΆ”κ°€ν•˜κ³  Optional을 λ°˜ν™˜ν•˜λ„λ‘ μž‘μ„±ν•  수 μžˆλ‹€.

func toJSON<T: Encodable>(_ data: T?) -> Data? {
    guard let data else { return nil }

    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    do {
        return try encoder.encode(data)
    } catch let error as EncodingError {
        // Common error handling here...
        return nil
    } catch {
        // Common error handling here...
        return nil
    }
}
struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")

let jsonData = try? toJSON(pear)
if let jsonData, let jsonData = String(data: jsonData, encoding: .utf8) {
    print(jsonData)
}


λ˜λŠ” λ‹€μŒκ³Ό 같이 μ‚¬μš©ν•˜λŠ” κ³³μ—μ„œ do-catch에 tryλ₯Ό μ‚¬μš©ν•΄ μ—λŸ¬μ— λŒ€ν•΄ 직접 μ²˜λ¦¬ν•˜κ±°λ‚˜, try?λ₯Ό μ‚¬μš©ν•΄ μ—λŸ¬λ₯Ό λ¬΄μ‹œν•˜κ³  쀑단할지λ₯Ό 직접 μ„ νƒν•˜λ„λ‘ ν•¨μˆ˜λ₯Ό μž‘μ„±ν•  μˆ˜λ„ μžˆλ‹€(Converting Errors to Optional Values λ₯Ό μ°Έκ³ ).

enum CastingError: Error {
    case inputIsNil
}

func toJSON<T: Encodable>(_ data: T?) throws -> Data {
    guard let data else { CastingError.inputIsNil }
    
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    
    do {
        return try encoder.encode(data)
    } catch {
        throw error
    }
}
struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")

// μ—λŸ¬λ₯Ό 직접 처리
do {
    let jsonData = try toJSON(pear)
    print(String(data: jsonData, encoding: .utf8)!)
} catch {
    print(error)
}

// try? 둜 μ—λŸ¬λ₯Ό Optional 처리
let jsonData = try? toJSON(pear)
if let jsonData, let jsonData = String(data: jsonData, encoding: .utf8) {
    print(jsonData)
}


μ—¬κΈ°μ„œλŠ” 두 번째 방법과 같이 Input Parameters λŠ” Optional이고, throwsλ₯Ό 던질 수 있으며, Return Types λŠ” Non-Optional이 λ˜λ„λ‘ μœ ν‹Έ ν•¨μˆ˜λ₯Ό μž‘μ„±ν•  것이닀.

enum CastingError: Error {
    case inputIsNil
}

func toJSON<T: Encodable>(_ data: T?) throws -> Data {
    guard let data else { throw CastingError.inputIsNil }

    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    return try encoder.encode(data)
}

func toDictionary<T: Encodable>(_ data: T?) throws -> Any {
    guard let data else { throw CastingError.inputIsNil }

    let jsonData = try toJSON(data)
    return try JSONSerialization.jsonObject(with: jsonData, options: .fragmentsAllowed)
}

5. Custom Decoding Methods for Firebase

class func data(
    withJSONObject obj: Any,
    options opt: JSONSerialization.WritingOptions = []
) throws -> Data
func decode<T>(
    _ type: T.Type,
    from data: Data
) throws -> T where T : Decodable

λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λ―€λ‘œ Anyλ₯Ό λ°›μ•„ T: Decodable을 λ°˜ν™˜ν•˜λ©΄ λœλ‹€.

enum CastingError: Error {
    case inputIsNil
}

func fromJSON<T: Decodable>(_ type: T.Type, from data: Data?) throws -> T {
    guard let data else { throw CastingError.inputIsNil }

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601

    return try decoder.decode(type, from: data)
}

func fromDictionary<T: Decodable>(_ type: T.Type, withJSONObject obj: Any?) throws -> T {
    guard let obj else { throw CastingError.inputIsNil }

    let data = try JSONSerialization.data(withJSONObject: obj, options: .fragmentsAllowed)
    return try fromJSON(type, from: data)
}

6. Examples

struct DataUtil {

    enum CastingError: Error {
        case inputIsNil
    }

    func toJSON<T: Encodable>(_ data: T?) throws -> Data {
        guard let data else { throw CastingError.inputIsNil }

        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted

        return try encoder.encode(data)
    }

    func toDictionary<T: Encodable>(_ data: T?) throws -> Any {
        guard let data else { throw CastingError.inputIsNil }

        let jsonData = try toJSON(data)
        return try JSONSerialization.jsonObject(with: jsonData, options: .fragmentsAllowed)
    }

    func fromJSON<T: Decodable>(_ type: T.Type, from data: Data?) throws -> T {
        guard let data else { throw CastingError.inputIsNil }

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601

        return try decoder.decode(type, from: data)
    }

    func fromDictionary<T: Decodable>(_ type: T.Type, withJSONObject obj: Any?) throws -> T {
        guard let obj else { throw CastingError.inputIsNil }

        let data = try JSONSerialization.data(withJSONObject: obj, options: .fragmentsAllowed)
        return try fromJSON(type, from: data)
    }

    func toUserDefaults<T: Encodable>(_ value: T, forkey: String) throws {
        let encodedValue = try PropertyListEncoder().encode(value)
        UserDefaults.standard.setValue(encodedValue, forKey: forkey)
    }

    func fromUserDefaults<T: Decodable>(_ type: T.Type, forkey: String) throws -> T? {
        guard let data = UserDefaults.standard.object(forKey: forkey) as? Data else { return nil }
        return try PropertyListDecoder().decode(type, from: data)
    }

}
class CardListTableViewController: UITableViewController {

    // Firestore
    var db: Firestore!

    var creditCards: [CreditCard] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // Database
        db = Firestore.firestore()
        // MARK: Firebase Firestore GET
        db.collection("creditCardList").addSnapshotListener { querySnapshot, error in
            guard let documents = querySnapshot?.documents else {
                print("ERROR: Fetching documents \(error!.localizedDescription)")
                return
            }
            self.creditCards = documents.compactMap {
                        let document: [String: Any] = $0.data()
                        guard let cardData = try? DataUtil().fromDictionary(CreditCard.self, withJSONObject: document) else { return nil }
                        return cardData
                    }.sorted { $0.rank < $1.rank }

            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
    }

    private func uploadCard(cardData: CreditCard) {
        guard let cardData = try? DataUtil().toDictionary(cardData) else { return }
        let encoder = JSONEncoder()
        guard let data = try? encoder.encode(data) else { return }
        guard let data = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
        db.collection("creditCardList").document("Item1").setData(data)
    }
}

μ΄λŸ°μ‹μœΌλ‘œ λ°˜λ³΅λ˜λŠ” Decoding 은 λ¬Όλ‘ , Encoding 처리 μž‘μ—…μ„ λΆ„λ¦¬μ‹œν‚¬ 수 μžˆλ‹€.


4. CodingKeys vs. Computed πŸ‘©β€πŸ’»

API 톡신을 ν•  λ•Œ μ•±μ—μ„œ μ‚¬μš©ν•˜λŠ” 객체와 APIκ°€ μ‚¬μš©ν•˜λŠ” 객체가 λ™μΌν•˜λ‹€λŠ” 보μž₯이 μ—†λ‹€. κ·Έλ ‡λ‹€κ³  API 톡신을 λ°›κ±°λ‚˜ 보낼 λ•Œ 맀번 객체λ₯Ό Converting ν•˜λŠ” 것은 맀우 μ†Œλͺ¨μ μΈ 일이닀. μ΄λŸ¬ν•œ 문제λ₯Ό Swift μ—μ„œλŠ” 두 가지 λ°©λ²•μœΌλ‘œ ν•΄κ²°ν•  수 μžˆλ‹€.

1. CodingKeys

struct CoinData: Codable {
    
    let time: String
    let coin: String
    let currency: String
    let rate: Double
    
    enum CodingKeys: String, CodingKey {
        case time
        case coin = "asset_id_base"
        case currency = "asset_id_quote"
        case rate
    }
    
}

κ°€μž₯ 보편적인 λ°©λ²•μœΌλ‘œ Swift κ°€ μ œκ³΅ν•˜λŠ” CodingKeys Enumerations λ₯Ό μ‚¬μš©ν•˜λŠ” 것이닀. μœ„μ™€ 같이 μž‘μ„±ν•˜λ©΄ 인코딩, λ””μ½”λ”© μ‹œ μžλ™μœΌλ‘œ Key κ°€ λ§€μΉ­λœλ‹€.

2. Computed

struct CoinData: Codable {
    
    let time: String
    private var asset_id_base: String
    var coin: String {
        get { asset_id_base }
        set { asset_id_base = newValue }
    }
    private var asset_id_quote: String
    var currency: String {
        get { asset_id_quote }
        set { asset_id_quote = newValue }
    }
    let rate: Double
    
    init(time: String, coin: String, currency: String, rate: Double) {
        self.time = time
        self.asset_id_base = coin
        self.asset_id_quote = currency
        self.rate = rate
    }

}

또 λ‹€λ₯Έ λ°©λ²•μœΌλ‘œλŠ” Computed Propertiesλ₯Ό μ‚¬μš©ν•˜λŠ” 것이닀. 단, 이 경우 Computed Properties λ₯Ό μ‚¬μš©ν–ˆκΈ° λ•Œλ¬Έμ— Memberwise Initializers κ°€ μƒμ„±λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 직접 Initializers λ₯Ό λ§Œλ“€μ–΄μ•Όν•œλ‹€.

μœ„ CodingKeys 와 Computed Properties λŠ” λ‹€μŒ μ½”λ“œλ₯Ό 톡해 ν…ŒμŠ€νŠΈ κ°€λŠ₯ν•˜λ‹€.

// Create a CoinData structure
let coinData = CoinData(time: "2024-01-23T12:34:56Z",
                        coin: "BTC",
                        currency: "USD",
                        rate: 40000.50)

// 1. toJSON(_:) test
do {
    let jsonData = try DataUtil().toJSON(coinData)
    let jsonString = String(data: jsonData, encoding: .utf8)
    print("CoinData as JSON:\n\(jsonString ?? "Unable to convert to JSON")")
} catch {
    print("Error converting CoinData to JSON: \(error)")
}

// 2. toDictionary(_:) test
do {
    let dataUtil = DataUtil()
    let jsonDictionary = try dataUtil.toDictionary(coinData)
    print("CoinData as Dictionary:\n\(jsonDictionary)")
} catch {
    print("Error converting CoinData to Dictionary: \(error)")
}

// Create a JSON String
let jsonData = """
{
    "time": "2024-01-23T12:34:56Z",
    "asset_id_base": "BTC",
    "asset_id_quote": "USD",
    "rate": 40000.50
}
""".data(using: .utf8)

// 3. fromJSON(_:from:) test
do {
    let coinDataFromJSON = try DataUtil().fromJSON(CoinData.self, from: jsonData)
    print("CoinData from JSON:\n\(coinDataFromJSON)")
} catch {
    print("Error converting JSON to CoinData: \(error)")
}

// Create a dictionary
let jsonDictionary: [String: Any] = [
    "time": "2024-01-23T12:34:56Z",
    "asset_id_base": "BTC",
    "asset_id_quote": "USD",
    "rate": 40000.50
]

// 4. fromDictionary(_:withJSONObject:) test
do {
    let coinDataFromDict = try DataUtil().fromDictionary(CoinData.self, withJSONObject: jsonDictionary)
    print("CoinData from Dictionary:\n\(coinDataFromDict)")
} catch {
    print("Error converting Dictionary to CoinData: \(error)")
}




Reference

  1. β€œCodable.” Apple Developer Documentation. accessed Jan. 16, 2024, Apple Developer Documentation - Swift/Swift Standard Library/Codable.
  2. β€œJSONSerialization.” Apple Developer Documentation. accessed Jan. 16, 2024, Apple Developer Documentation - Foundation/Archives and Serialization/JSONSerialization.