Swift 4-----JSON 解析的原生支持

Apple 在 Swift 4 的 Foundation 的模塊中添加了對(duì) JSON 解析的原生支持。
基礎(chǔ)

如果你的 JSON 數(shù)據(jù)結(jié)構(gòu)和你使用的 Model 對(duì)象結(jié)構(gòu)一致的話,那么解析過程將會(huì)非常簡(jiǎn)單。

下面是一個(gè) JSON 格式的說明:

{
    "name": "Endeavor",
    "abv": 8.9,
    "brewery": "Saint Arnold",
    "style": "ipa"
}

對(duì)應(yīng)的 Swift 數(shù)據(jù)結(jié)構(gòu)如下:

enum BeerStyle : String {
    case i
    case s
    case k
    // ...
}
 
struct Beer {
    let name: String
    let brewery: String
    let style: BeerStyle
}

為了將 JSON 字符串轉(zhuǎn)化為 Beer 類型的實(shí)例,我們需要將 Beer 類型標(biāo)記為 Codable。

Codable 實(shí)際上是 Encodable & Decodable 兩個(gè)協(xié)議的組合類型,所以如果你只需要單向轉(zhuǎn)換的話,你可以只選用其中一個(gè)。該功能也是 Swift 4 中引入的最重要新特性之一。

Codable 帶有默認(rèn)實(shí)現(xiàn),所以在大多數(shù)情形下,你可以直接使用該默認(rèn)實(shí)現(xiàn)進(jìn)行數(shù)據(jù)轉(zhuǎn)換。

enum BeerStyle : String, Codable {
   // ...
}
 
struct Beer : Codable {
   // ...
}

下面只需要?jiǎng)?chuàng)建一個(gè)解碼器:

let jsonData = jsonString.data(encoding: .utf8)!
let decoder = JSONDecoder()
let beer = try! decoder.decode(Beer.self, for: jsonData)

這樣我們就將 JSON 數(shù)據(jù)成功解析為了 Beer 實(shí)例對(duì)象。因?yàn)?JSON 數(shù)據(jù)的 Key 與 Beer 中的屬性名一致,所以這里不需要進(jìn)行自定義操作。

但是,現(xiàn)實(shí)中不可能一直都是完美情形,很大幾率存在 Key 值與屬性名不匹配的情形。

自定義鍵值名

通常情形下,API 接口設(shè)計(jì)時(shí)會(huì)采用 snake-case 的命名風(fēng)格,但是這與 Swift 中的編程風(fēng)格有著明顯的差異。

為了實(shí)現(xiàn)自定義解析,我們需要先去看下 Codable 的默認(rèn)實(shí)現(xiàn)機(jī)制。

默認(rèn)情形下 Keys 是由編譯器自動(dòng)生成的枚舉類型。該枚舉遵守 CodingKey 協(xié)議并建立了屬性和編碼后格式之間的關(guān)系。

為了解決上面的風(fēng)格差異需要對(duì)其進(jìn)行自定義,實(shí)現(xiàn)代碼:

struct Beer : Codable {
      // ...
      enum CodingKeys : String, CodingKey {
          case name
          case abv = "alcohol_by_volume"
          case brewery = "brewery_name"
          case style
    }
}

現(xiàn)在我們將 Beer 實(shí)例轉(zhuǎn)化為 JSON ,看看自定義之后的 JSON 數(shù)據(jù)格式:

let encoder = JSONEncoder()
let data = try! encoder.encode(beer)
print(String(data: data, encoding: .utf8)!)
輸出如下:
{"style":"ipa","name":"Endeavor","alcohol_by_volume":8.8999996185302734,"brewery_name":"Saint Arnold"}

上面的輸出格式對(duì)閱讀起來并不是太友好。不過我們可以設(shè)置 JSONEncoder 的 outputFormatting 屬性來定義輸出格式。
默認(rèn) outputFormatting 屬性值為 .compact,輸出效果如上。如果將其改為 .prettyPrinted 后就能獲得更好的閱讀體檢。

encoder.outputFormatting = .prettyPrinted

效果如下:

{
  "style" : "ipa",
  "name" : "Endeavor",
  "alcohol_by_volume" : 8.8999996185302734,
  "brewery_name" : "Saint Arnold"
}

JSONEncoder 和 JSONDecoder 其實(shí)還有很多選項(xiàng)可以自定義設(shè)置。其中有一個(gè)常用的需求就是自定義時(shí)間格式的解析。

時(shí)間格式處理

JSON 沒有數(shù)據(jù)類型表示日期格式,因此需要客戶端和服務(wù)端對(duì)序列化進(jìn)行約定。通常情形下都會(huì)使用 ISO 8601 日期格式并序列化為字符串。

提示:nsdateformatter.com 是一個(gè)非常有用的網(wǎng)站,你可以查看各種日期格式的字符串表示,包括 ISO 8601。

其他格式可能是參考日期起的總秒(或毫秒)數(shù),并將其序列化為 JSON 格式中的數(shù)字類型。

之前,我們必須自己處理這個(gè)問題。在數(shù)據(jù)結(jié)構(gòu)中使用屬性接收該字符串格式日期,然后使用 DateFormatter 將該屬性轉(zhuǎn)化為日期,反之亦然。

不過 JSONEncoder 和 JSONDecoder 自帶了該功能。默認(rèn)情況下,它們使用 .deferToDate 處理日期,如下:

struct Foo : Encodable {
    let date: Date
}
 
let foo = Foo(date: Date())
try! encoder.encode(foo)
{
  "date" : 519751611.12542897
}

當(dāng)然,我們也可以選用 .iso8601 格式:

encoder.dateEncodingStrategy = .iso8601
{
  "date" : "2017-06-21T15:29:32Z"
}
```
其他日期編碼格式選擇如下:

.formatted(DateFormatter) - 當(dāng)你的日期字符串是非標(biāo)準(zhǔn)格式時(shí)使用。需要提供你自己的日期格式化器實(shí)例。
.custom((Date, Encoder) throws -> Void ) - 當(dāng)你需要真正意義上的自定義時(shí),使用一個(gè)閉包進(jìn)行實(shí)現(xiàn)。
.millisecondsSince1970、 .secondsSince1970 - 這在 API 設(shè)計(jì)中不是很常見。 由于時(shí)區(qū)信息完全不在編碼表示中,所以不建議使用這樣的格式,這使得人們更容易做出錯(cuò)誤的假設(shè)。
對(duì)日期進(jìn)行 Decoding 時(shí)基本上是相同的選項(xiàng),但是 .custom 形式是 .custom((Decoder) throws -> Date ),所以我們給了一個(gè)解碼器并將任意類型轉(zhuǎn)換為日期格式。

#####浮點(diǎn)類型處理

浮點(diǎn)是 JSON 與 Swift 另一個(gè)存在不匹配情形的類型。如果服務(wù)器返回的事無效的 "NaN" 字符串會(huì)發(fā)生什么?無窮大或者無窮大?這些不會(huì)映射到 Swift 中的任何特定值。

默認(rèn)的實(shí)現(xiàn)是 .throw,這意味著如果上述數(shù)值出現(xiàn)的話就會(huì)引發(fā)錯(cuò)誤,不過對(duì)此我們可以自定義映射。

```
{
   "a": "NaN",
   "b": "+Infinity",
   "c": "-Infinity"
}
struct Numbers {
  let a: Float
  let b: Float
  let c: Float
}
decoder.nonConformingFloatDecodingStrategy =
  .convertFromString(
      positiveInfinity: "+Infinity",
      negativeInfinity: "-Infinity",
      nan: "NaN")
 
let numbers = try! decoder.decode(Numbers.elf, from: jsonData)
dump(numbers)
```
上述處理后:
```
__lldb_expr_71.Numbers
  - a: inf
  - b: -inf
  - c: nan
```
當(dāng)然,我們也可以使用 JSONEncoder 的 nonConformingFloatEncodingStrategy 進(jìn)行反向操作。


#####Data 處理

有時(shí)候服務(wù)端 API 返回的數(shù)據(jù)是 base64 編碼過的字符串。
對(duì)此,我們可以在 JSONEncoder 使用以下策略:
```
.base64
.custom((Data, Encoder) throws -> Void)
```
反之,編碼時(shí)可以使用:
```
.base64
.custom((Decoder) throws -> Data)
```
顯然,.base64 時(shí)最常見的選項(xiàng),但如果需要自定義的話可以采用 block 方式。

#####Wrapper Keys

通常 API 會(huì)對(duì)數(shù)據(jù)進(jìn)行封裝,這樣頂級(jí)的 JSON 實(shí)體 始終是一個(gè)對(duì)象。

例如:

```
{
  "beers": [ {...} ]
}
```
在 Swift 中我們可以進(jìn)行對(duì)應(yīng)處理:

```
struct BeerList : Codable {
    let beers: [Beer]
}
```
因?yàn)殒I值與屬性名一致,所有上面代碼已經(jīng)足夠了。

#####Root Level Arrays

如果 API 作為根元素返回?cái)?shù)組,對(duì)應(yīng)解析如下所示:

```
let decoder = JSONDecoder()
let beers = try decoder.decode([Beer].self, from: data)
```
需要注意的是,我們?cè)谶@里使用 Array 作為類型。只要 T 可解碼,Array 就可解碼。

#####Dealing with Object Wrapping Keys

另一個(gè)常見的場(chǎng)景是,返回的數(shù)組對(duì)象里的每一個(gè)元素都被包裝為字典類型對(duì)象。

```
[
  {
    "beer" : {
      "id": "uuid12459078214",
      "name": "Endeavor",
      "abv": 8.9,
      "brewery": "Saint Arnold",
      "style": "ipa"
    }
  }
]
```
你可以使用上面的方法來捕獲此 Key 值,但最簡(jiǎn)單的方式就是認(rèn)識(shí)到該結(jié)構(gòu)的可編碼的實(shí)現(xiàn)形式。

如下:

```
[[String:Beer]]
```
或者更易于閱讀的形式:
```
Array
```
與上面的 Array 類似,如果 K 和 T 是可解碼 Dictionary就能解碼。
```
let decoder = JSONDecoder()
let beers = try decoder.decode([[String:Beer]].self, from: data)
dump(beers)
 1 element
  ? 1 key/value pair
    ? (2 elements)
      - key: "beer"
      ? value: __lldb_expr_37.Beer
        - name: "Endeavor"
        - brewery: "Saint Arnold"
        - abv: 8.89999962
        - style: __lldb_expr_37.BeerStyle.ipa
```
#####更復(fù)雜的嵌套

有時(shí)候 API 的響應(yīng)數(shù)據(jù)并不是那么簡(jiǎn)單。頂層元素不一定只是一個(gè)對(duì)象,而且通常情況下是多個(gè)字典結(jié)構(gòu)。

例如:
```
{
    "meta": {
        "page": 1,
        "total_pages": 4,
        "per_page": 10,
        "total_records": 38
    },
    "breweries": [
        {
            "id": 1234,
            "name": "Saint Arnold"
        },
        {
            "id": 52892,
            "name": "Buffalo Bayou"
        }
    ]
}
```
在 Swift 中我們可以進(jìn)行對(duì)應(yīng)的嵌套定義處理:
```
struct PagedBreweries : Codable {
    struct Meta : Codable {
        let page: Int
        let totalPages: Int
        let perPage: Int
        let totalRecords: Int
        enum CodingKeys : String, CodingKey {
            case page
            case totalPages = "total_pages"
            case perPage = "per_page"
            case totalRecords = "total_records"
        }
    }
 
    struct Brewery : Codable {
        let id: Int
        let name: String
    }
 
    let meta: Meta
    let breweries: [Brewery]
}
```
該方法的最大優(yōu)點(diǎn)就是對(duì)同一類型的對(duì)象做出不同的響應(yīng)(可能在這種情況下,“brewery” 列表響應(yīng)中只需要 id 和 name 屬性,但是如果查看詳細(xì)內(nèi)容的話則需要更多屬性內(nèi)容)。因?yàn)樵撉樾蜗?Brewery 類型是嵌套的,我們依舊可以在其他地方進(jìn)行不同的 Brewery 類型實(shí)現(xiàn)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,026評(píng)論 4 61
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評(píng)論 19 139
  • 幽靈一般的5% Atul Gawande 醫(yī)生在他的書《醫(yī)生的修煉》里面講到一個(gè)真實(shí)的故事。 Atual剛剛為一個(gè)...
    南瓜小子Eric閱讀 1,703評(píng)論 0 6
  • No.47 心理學(xué)上說,生病的時(shí)候人最容易脆弱,我也不例外,但生病的時(shí)候,我從來不哭。 以前身體不好的時(shí)候,總是發(fā)...
    水淺_bling閱讀 640評(píng)論 3 1

友情鏈接更多精彩內(nèi)容