天天看點

Codable 自定義解析 JSON

大多數現代應用程式的共同點是,它們需要對各種形式的資料進行編碼或解碼。無論是通過網絡下載下傳的JSON資料,還是存儲在本地的模型的某種形式的序列化表示形式,對于幾乎任何 Swift 代碼庫而言,能夠可靠地編碼和解碼不同的資料都是必不可少的。

這就是為什麼Swift的Codable API成為Swift 4.0的新功能一部分時具有如此重要的重要原因——從那時起,它已發展成為一種标準的,健壯的機制,可以在Apple的各種平台中使用編碼和解碼包括伺服器端Swift。

Codable

之是以如此出色,是因為它與Swift工具鍊緊密內建,進而使編譯器可以自動合成大量編碼和解碼各種值所需的代碼。但是,有時我們确實需要自定義序列化時值的表示方式——是以,本周,讓我們看一下可以調整

Codable

實作來做到這一點的幾種不同方式。

修改 Key

讓我們從一種基本的方式開始,我們可以通過修改用作序列化表示形式一部分的鍵來自定義類型的編碼和解碼方式。假設我們正在開發一款用于閱讀文章的應用,而我們的一個核心資料模型如下所示:

struct Article: Codable {
    var url: URL
    var title: String
    var body: String
}           

複制

我們的模型目前使用完全自動合成的

Codable

實作,這意味着其所有序列化鍵都将比對其屬性的名稱。但是,我們将從中解碼

Article

值的資料(例如,從伺服器下載下傳的JSON)可能會使用略有不同的命名約定,進而導緻預設解碼失敗。

幸運的是,這一問題很容易解決。要自定義

Codable

在解碼(或編碼)我們的

Article

類型的執行個體時将使用哪些鍵,我們要做的就是在其中定義一個

CodingKeys

枚舉,并為與我們希望自定義的鍵比對的大小寫配置設定自定義原始值——像這樣:

extension Article {
    enum CodingKeys: String, CodingKey {
        case url = "source_link"
        case title = "content_name"
        case body
    }
}           

複制

通過上述操作,我們可以繼續利用編譯器生成的預設實作進行實際的編碼工作,同時仍使我們能夠更改将用于序列化的鍵的名稱。

雖然上面的技術非常适合當我們想要使用完全自定義的鍵名時,但是如果我們隻希望

Codable

使用屬性名的

snake_case

版本(例如,将

backgroundColor

轉換為

background_color

),那麼我們可以簡單地更改JSON解碼器的

keyDecodingStrategy

var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase           

複制

以上兩個API的優點在于,它們使我們能夠解決Swift模型與用于表示它們的資料之間的不比對問題,而無需我們修改屬性名稱。

忽略 Key

能夠自定義編碼鍵的名稱确實很有用,但有時我們可能希望完全忽略某些鍵。例如,現在我們說我們正在開發一個記筆記應用程式,并且使使用者能夠将各種筆記分組在一起以形成一個可以包括本地草稿的

NoteCollection

struct NoteCollection: Codable {
    var name: String
    var notes: [Note]
    var localDrafts = [Note]()
}           

複制

但是,雖然将

localDrafts

納入

NoteCollection

模型确實很友善,但可以說,我們不希望在序列化或反序列化此類集合時包含這些草稿。這樣做的原因可能是每次啟動應用程式時為使用者提供整潔的狀态,或者是因為我們的伺服器不支援草稿。

幸運的是,這也可以輕松完成,而不必更改

NoteCollection

的實際

Codable

實作。如果像以前一樣定義一個

CodingKeys

枚舉,而隻是省略

localDrafts

,那麼在對

NoteCollection

值進行編碼或解碼時,将不會考慮該屬性:

extension NoteCollection {
    enum CodingKeys: CodingKey {
        case name
        case notes
    }
}           

複制

為了使以上功能正常運作,我們要省略的屬性必須具有預設值——在這種情況下,

localDrafts

已經具有預設值。

建立比對的結構

到目前為止,我們隻是在調整類型的編碼鍵——盡管這樣做通常可以使您受益匪淺,但有時我們需要對

Codable

自定義進行進一步的調整。

假設我們正在建構一個包含貨币換算功能的應用,并且正在将給定貨币的目前匯率下載下傳為 JSON 資料,如下所示:

{
    "currency": "PLN",
    "rates": {
        "USD": 3.76,
        "EUR": 4.24,
        "SEK": 0.41
    }
}           

複制

然後,在我們的Swift代碼中,我們想要将此類JSON響應轉換為

CurrencyConversion

執行個體——每個執行個體都包含一個

ExchangeRate

條目數組——每個币種對應一個:

struct CurrencyConversion {
    var currency: Currency
    var exchangeRates: [ExchangeRate]
}

struct ExchangeRate {
    let currency: Currency
    let rate: Double
}           

複制

但是,如果我們僅僅隻是使以上兩個模型都符合

Codable

,我們将再次導緻Swift代碼與我們要解碼的JSON資料不比對。但是這次,不隻是關鍵字名稱的問題——結構上有根本的不同。

當然,我們可以修改Swift模型的結構,使其與JSON資料的結構完全比對,但這并不總是可行的。盡管擁有正确的序列化代碼很重要,但是擁有适合我們實際代碼庫的模型結構也同樣重要。

相反,讓我們建立一個新的專用類型——它将在JSON資料中使用的格式與Swift代碼的結構體之間架起一座橋梁。在這種類型中,我們将能夠封裝将JSON匯率字典轉換為一系列

ExchangeRate

模型所需的所有邏輯,如下所示:

private extension ExchangeRate {
    struct List: Decodable {
        let values: [ExchangeRate]

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let dictionary = try container.decode([String : Double].self)

            values = dictionary.map { key, value in
                ExchangeRate(currency: Currency(key), rate: value)
            }
        }
    }
}           

複制

使用上述類型,我們現在可以定義一個私有屬性,該名稱與用于其資料的JSON密鑰相比對——并使我們的

exchangeRates

屬性僅充當該私有屬性的面向公衆的代理:

struct CurrencyConversion: Decodable {
    var currency: Currency
    var exchangeRates: [ExchangeRate] {
        return rates.values
    }
    
    private var rates: ExchangeRate.List
}           

複制

上面的方法起作用的原因是,在對值進行編碼或解碼時,永遠不會考慮計算屬性。

當我們想使我們的Swift代碼與使用非常不同的結構的JSON API相容時,上述技術可能是一個很好的工具——且無需完全從頭實作

Codable

轉換值

在解碼時,尤其是在使用我們無法控制的外部JSON API進行解碼時,一個非常常見的問題是,以與Swift的嚴格類型系統不相容的方式對類型進行編碼。例如,我們要解碼的JSON資料可能使用字元串來表示整數或其他類型的數字。

讓我們來看看一種可以讓我們處理這些值的方法,再次以一種自包含的方式,它不需要我們編寫完全自定義的

Codable

實作。

我們本質上想要做的是将字元串值轉換為另一種類型,以

Int

為例。我們将從定義一個協定開始,該協定使我們可以将任何類型都标記為

StringRepresentable

,這意味着可以将其轉換為字元串表示形式,也可以将其從字元串表示形式轉換為我們要的類型:

struct StringBacked<Value: StringRepresentable>: Codable {
    var value: Value
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        
        guard let value = Value(string) else {
            throw DecodingError.dataCorruptedError(
                in: container,
                debugDescription: "Failed to convert an instance of \(Value.self) from '\(string)'"
            )
        }
        
        self.value = value
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value.description)
    }
}           

複制

就像我們以前為相容JSON的基礎存儲建立私有屬性的方式一樣,現在我們可以對編碼後由字元串後端的任何屬性執行相同的操作,同時仍将資料适當地公開給其他Swift代碼類型。這是一個針對視訊類型的

numberOfLikes

屬性執行此操作的示例:

struct Video: Codable {
    var title: String
    var description: String
    var url: URL
    var thumbnailImageURL: URL
    
    var numberOfLikes: Int {
        get { return likes.value }
        set { likes.value = newValue }
    }
    
    private var likes: StringBacked<Int>
}           

複制

在必須手動為屬性定義

setter

getter

的複雜性與必須回退到完全自定義的

Codable

實作的複雜性之間,這裡肯定有一個折中——但是對于上述

Video

結構體這樣的類型,它在其中僅具有一個屬性需要自定義,使用私有支援屬性可能是一個不錯的選擇。

結語

盡管編譯器能夠自動合成不需要任何形式的自定義的所有類型的

Codable

支援,這真是太棒了,但是我們能夠在需要時進行自定義,這一事實同樣是太棒了。

更好的是,這樣做實際上并不需要我們完全放棄自動生成的代碼,而是采用手動實作——很多時候,可以稍微調整類型的編碼或解碼方式,同時仍然讓編譯器做大部分繁重的工作。

謝謝閱讀!

譯自 John Sundell 的 Customizing Codable types in Swift