【Swift】Swifterに学ぶ SwiftのJSONハンドリング

 Swiftでスキーマの不明なJSONをパースする機会があったので、
 [SwiftからTwitterAPIを扱うFramework「Swifter」](https://github.com/mattdonnelly/Swifter)のJSONパーサはどうなっているのか調べてみました。

 ⚠️本題は[ここ](#swifterjson)から始まります。**前置きが長い。**

また、**Swiferからコードを引用した場合は「Swifter/ファイル名.swift」を明記**しています。

## 忙しい人のための結論

[SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)に似た実装になっていましたが、だいぶコンパクトにまとめられています。
ここから拡張して、`URL`や`Date`に自動変換するようなライブラリにもできるでしょう。

## SwiftでJSONを扱うには
 SwiftによるJSONパースの方法としては、主に以下のような方法があげられるかと思います。
 (多分他にもあると思いますが、私が思いつくのはこの二つです)

  * `JSONDecoder`に通して構造体インスタンスを生成
  * `JSONSerilalization:jsonObject` でパースしたのち、`as? [String: Any]`でDictionaryにキャスト

### それぞれのメリット、デメリット
 `JSONDecoder`を使用する方法では、

  * 構造体インスタンスになるため、**コード補完が効く**
  * `JSONEncoder`を使用することで、同じ構造体を**エンコード処理でも使いまわせる**
  * **スキーマが定義されていないと変換できない**(つまり、外部APIの処理には向かない)

 これに対し`[Stirng: Any]`にキャストする方法では、

  * **スキーマがなくても変換可能**
  * 各プロパティの値が`Any`なので、**どこかで再キャストしなければならない**
  * 一階層下るごとに**Force Unwrapする必要がある**
  * **配列内のJSONオブジェクトは変換されない**

 といったメリット、デメリットがあります。

### 外部APIのレスポンス処理に適したパース方法
 スキーマが不変であることを保証できる場合は、`JSONEncoder`も一つの実装方式としてはアリでしょう。
 しかし、プロパティの多いJSONオブジェクトを扱おうとすると**構造体の定義が面倒**です。
 また、ほとんどのAPIサービスにおいて**APIのレスポンスの形式が変わらないという保証はありません。** 
 自分で使う範囲ならば、再びAPIリファレンスを読んで…構造体を調整して…などで済むかもしれませんが、不特定多数により使用される可能性のあるFramework、また実際にローンチすることまで考えているようなAppではそうはいきません。
 
 このように、JSONパース処理の実装では**スキーマに対する柔軟性**がとても重要になってきます。

## Swifterのパース処理

 では、SwifterはどのようにJSONをパースしているのでしょうか。TwitterAPIは一つのレスポンスに含まれるプロパティ数も非常に多く、構造体で全てを記述しきるのはとても現実的とはいえません。
 また、新しい機能をサポートするAPIも随時追加されています。これではとても対応しきれません。
 
 そこで、Swifterでは`[String: Any]`に変換し、さらに`Swifter.JSON`の配列に変換するという処理方法をとっています。

 Swifterでは一つのAPI呼び出しに`success`、`failure`というコールバック関数を設定することができます。
 例えば、ホームタイムラインを取得する関数は以下のように呼び出します。(一部引数を省略しています)

``` swift:Swifter/SwifterTimelines.swift 
 Swifter.getHomeTimeline(count: 20, trimUser: false, includeEntities: true, success:  { (json) in
    // process
 }, failure:  { (error) in
    // error handling
 })
 ```

 API呼び出しに成功すると、`success(JSON)`が実行されます。
  **前置きが長くなりましたが、** この記事ではこの`JSON`について説明していきます。

## Swifter.JSON
 `Swifter.JSON`(以下、単純に `JSON` と記述)の実体を調べてみます。

```swift:Swifter/JSON.swift 
public enum JSON : Equatable, CustomStringConvertible {
    ...
}
```

なんということでしょう。`JSON`の実体はクラスではなく単なる列挙型だったのです。クラスとはなんだったのか。**もうワケがわかりません。**
ですがここは一旦落ち着いて、もう少し噛み砕いてソースを読んでいきます。

まず、`JSON`のソースを以下の3つに分割して**勝手に名前をつけます**。(実際にはこれ以外にもextensionなどが存在し、より汎用的に使えるようになっています。ここではJSONパースに使われている部分のみをピックアップしています。)

* 付属型enum部
* キャスト部
* プロパティ部

### 付属型enum部
 `JSON.swift`の一番上に記述されているこれです。

 ```swift:Swifter/JSON.swift 
 case string(String)
 case number(Double)
 case object(Dictionary<String, JSON>)
 case array(Array<JSON>)
 case bool(Bool)
 case null
 case invalid
```

JSONは様々な値を持ちます。文字列に整数、浮動小数点数、真偽値、さらにそれらに階層構造をもたらすオブジェクト、配列…さらに細かく区分するとすれば、URLや特有の属性値などが記述されることもあるでしょう。
ここでは、そのような多様な値を`JSON`で一元的に管理するために「**付属型enum**」を使用しています。これにより各要素に値を持たせることができ、またパターンマッチによって値を取り出すことができます。

(こちらの記事を参考に「付属型enum」と呼称しています)
> 
> [Swiftの列挙型(enum)おさらい](https://qiita.com/hachinobu/items/392c96820588d1c03b0c#付属型enumassociated-value)
>

### キャスト部(イニシャライザ部)
`JSON`のイニシャライザは `Any`型を受け入れるようになっており、内部で自動変換されたのち自身を初期化します。この際の処理は(単純に原理のみ記述すると)以下のようになっています。

``` swift
init(_ value: Any){
    switch value {
    
    case let string as String:
        self = .string(string)
        
    case let number as Double:
        self = .number(number)
        
    case let bool as Bool:
        self = .bool(bool)
        
    default:
        self = .null
        
    }
}
```

switch文中に何度も出てくる`case let U as T`は、「**switchに入力された変数がT型にキャストできれば、変数Uにキャスト済の値を代入しcase以下を実行する**」という動作になります。これについてはこの記事が参考になるかと思います。

> 
> [Swiftの「case is」とか「case as」って何なの？](https://qiita.com/kawano108/items/f33d9939fc2a6e0d1d4d)
>

イニシャライザに`Data`を渡して初期化しようとすると、`JSON`は内部で`JSONSerialization.jsonObject`を呼び出し、さらにその値を`JSON`のイニシャライザに渡します。
先述の通り、`JSONSerialization.jsonObject`は`[String: Any]`にキャストできます。
そのため、イニシャライザは次の処理を実行します。

```swift:Swifter/JSON.swift
case let dict as [String: Any]:
    var newDict = [String: JSON]()
    for (key, value) in dict {
        newDict[key] = JSON(value)
    }
    self = .object(newDict)
```

`[String: Any]`を`[String: JSON]`、つまり**Dictionaryのvalueを`JSON`に変換してしまう**のです。
この際再び`value`が`JSON`のイニシャライザに渡され、`Double`、`String`、`[JSON]`、`Array<JSON>`等々さまざまな型にキャストされます。これが再帰的に繰り返されることで、やがて **`[String: Any]`は完全に`[String: JSON]`にキャストされます。**
結果的にJSON内の全てのデータを列挙型の値に確定できるため、**取り出す際にforce unwrapする必要がなくなります**。

### プロパティ部

さて、イニシャライザによってキャストは終了しましたが、このままではJSON中の各プロパティの値がenumのままになってしまいます。「**各要素の値を取り出す処理**」が必要です。
先ほど、「付属型enumの要素から値を取り出すには、パターンマッチが必要」と書きました。
では、このパターンマッチ処理を実際に記述するとどのぐらいの分量になるでしょうか。

``` swift
guard case .number(let value) = self else {
    return nil
}
```

この量です。一度取り出すのに3行必要になります。これでは**Swiftyのスの字もありません**。
これを避けるため、`JSON`では取り出す値の型ごとにパターンマッチ処理が記述されています。また各マッチ処理は算出型プロパティのgetterとして記述されているため、`JSON`から値を取り出す場合は

    json[0]["id"].string // -> String型の値を取り出す

    json[0]["id"]["likes_count"].integer // -> Int型の値を取り出す

このように書くことができます。非常にシンプルです。

## Summary

* 外部API等を使用する場合は`JSONEncoder`では処理しきれない部分もある
* Swiftの列挙型は色々でき(すぎ)る
* `[String: Any]`のままでは使いにくいが、適切に変換すれば柔軟に対応可能