JUNSON - 様々な方法でデコード・エンコードできるJSONライブラリを作りました(Swift3)

## JUNSON
iOSアプリに限らずクライアントアプリケーションとJSONは切っても切れない関係にあります。
そのおかげもあり、Swift界隈には多種多様なJSONライブラリが存在しています。
その中、自分もとあるきっかけでJSONライブラリ [JUNSON] (https://github.com/SatoshiN21/JUNSON) を作成しました！

Github - SatoshiN21/JUNSON
https://github.com/SatoshiN21/JUNSON

**JUNSON**には以下のような特徴があります。

- Genericsを活用した型指定不要の**デコード**処理
- デコード失敗時のハンドリングを選択可能
    - デフォルト値で置き換え(like [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON))
    - Optional値
    - try-Catch
- 様々なオブジェクトを直感的にJSONへとエンコード可能
- Swift3対応、iOS8.0以上

## 作ろうと思った背景
[社内のハイパーiOSエンジニア](https://github.com/muukii)が唐突に

*「そうだ、JSONライブラリ作ろう」*

と言い出しました。
現在弊社のプロダクトを徐々にSwift3に移行していく中、主にJSONパーサとして使用していたSwiftyJSONが当時はSwift3に対応しておらず、
またSwiftyJSONにもっとこういう機能もあったらいいのになぁという不満点もちらほら出てきたこともあり、
だったら社内で作ってしまおうということになりました。

また今回は最初から1つのプロダクトに全員がコミットするスタイルではなく、とりあえず各々仕様を考え
いわゆる **"ぼくのかんがえたさいきょうのJSONらいぶらり"** を作ってみようぜ、というきっかけでJUNSONの実装を開始しました。
かなり受け身なきっかけで開始したJSONライブラリですが、個人的にはなかなか面白いものが作れたので、今回こちらの場を借りて紹介させて頂きます。

※ちなみにその[ハイパーiOSエンジニア](https://github.com/muukii)は**爆速**で
[JAYSON](https://github.com/muukii/JAYSON)というライブラリをリリースしています！
Swifty-JSONの柔軟性とtry-Catchを用いた厳格さを併せ持つパーサで
強力なログ出力機能がとても気になっています。
QiitaにてJAYSONについて投稿されてますので、こちらも是非チェックしてみて下さい！

[Linux対応]SwiftyJSONより厳しめなJSONライブラリ作りました (JAYSON)
http://qiita.com/muukii0803/items/2a01f6578064ca5dbf3b

## どんな事ができるの？
とりあえずざっくりどのような事ができるのか、こちらのJSONを使って説明してみたいと思います。
今回は[SWAPI - The Star Wars API](https://swapi.co/)をベースにJSONを改変しつつ説明していきます。

```json:star_wars.json
{
	"gender": "male", 
	"height": 1.72, 
	"name": "Luke Skywalker"
}
```
このJSONをデータ・モデルとして定義した以下の構造体にデコードします。 `JUNSONDecodable` と `JUNSONDefaultValue` については後ほど説明します。

```swift
struct Person: JUNSONDecodable, JUNSONDefauleValue {

    let name: String
    let height: Double
    let gender: Gender?
    
    static func decode(junson json: AnyJUNSON) -> Person? {
        let normal = json.asDefault
        
        return Person(name: normal["name"].decode(),
                      height: normal["height"].decode(),
                      gender: normal.asOptional["gender"].decode(trans: genderTransformer))
    }
    
    static var defaultValue: Person {
        return Person(name: "", height: 0, gender: nil)
    }
}
```

あとはJSONを読み込んだString(or Data,Any)を以下ような形でJUNSONに取り込めばデコード可能です

```swift
let persons: Person = JUNSON(string:json).decode()
```

デコード処理がとってもスッキリしました！
こちらの場合は、仮にJSONのデコードに失敗した場合、defaultValueで定義しているオブジェクトが指定されます。

## JUNSONの登場人物
JSONオブジェクトをコントロールするクラスには以下の3種類を用意しています。全てprotocolの**AnyJUNSON**を実装しており、相互変換が可能です。

> - JUNSON
	- デコード失敗時にデフォルト値に差し替えるJUNSONクラス。他のJUNSONから `.asDefault` でJUNSONに変更
- OptionalJUNSON
	- デコード失敗時にnilで差し替えるJUNSONクラス。他のJUNSONから `.asOptional` でOptionalJUNSONに変更
- TryJUNSON
	- デコード失敗をtry-Catch機構でハンドリングするJUNSONクラス。他のJUNSONから `.asTry` でTryJUNSONに変更

デコード・エンコード時に使用するprotocolは**JUNSONDecodable**と**JUNSONEncodable**の2つ存在します。
デコード・エンコード対象に各protocolを実装します。また、上記JUNSONを使用し、失敗時にデフォルト値を指定したい場合は別途**JUNSONDefaultValue**を実装します。
> - JUNSONDecodable
	- class/structなどをJUNSONのデコード対象とする為のprotocol.
	- **String,Double,Float,Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64**実装済
- JUNSONEncodable
	- class/structなどをJUNSONのエンコード対象とする為のprotocol.
	- **String,NSNumber,Double,Float,Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64,NSNull**実装済
- JUNSONDefaultValue
    - デコード失敗時のデフォルト値を定義可能なprotocol.
    - **String,Double,Float,Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64**実装済

基本的に列挙型含めJUNSONDecodableを実装すればデコード対象となりますが、*Date*など、シチュエーションによってはデコード方法を切り替えたい場合があります。
その際には**JUNSONTransformer**を使うことで、指定のDecodableなオブジェクトを別のものに変換させる事ができます。

> - JUNSONTransformer<T:JUNSONDecodable,Z>
	- Decodableなvalueをその他の形式に変換する(例えばStringを受け取ってDateに変換)

### 3種類のJUNSON
上記で紹介した通り、JUNSONには3種類存在します。それぞれの特徴について説明します。
今回は簡単に以下のJSONからauthTokenの取得してみます。

```json:
{
    "result_code": 200,
    "response": {
        "auth_token": "56eB44Gu5ZCN5YmN44Gv6ZW35Z2C5oKf5b+X44Gn44GZ44CC"
        ...
    }
}
```

#### JUNSON(デフォルト値)

```swift
let junson = JUNSON(string:string)
let authToken: String = junson["response"].decode(key:"auth_token")
```
デコード失敗時デフォルト値で置き換えたい場合、JUNSONを使用します。
JSONのtree階層を潜る場合は**SwiftyJSON**のようにsubscriptを使う事ができます。
`.decode(key: String)`をキーを指定し値を取得します。
以下のように書くこともできます。

```swift
let authToken: String = junson["response"]["auth_token"].decode()
```
#### OptionalJUNSON(Optional)

```swift
let junson = JUNSON(string:string).asOptional // もしくはOptionalJUNSON(string:string)
let authToken: String? = junson["response"].decode(key:"auth_token")
```

値をOptional型で取得したい場合はOptionalJUNSONを使用します。仮にキー値の取得に失敗した場合、nilが入ります。

#### TryJUNSON(try-Catch)

```swift
let junson = JUNSON(string:string).asTry // もしくはTryJUNSON(string:string)
do {
    let authToken: String = junson["response"].decode(key:"auth_token")
} catch {
}
```

TryJUNSONはデコード失敗時、JUNSONErrorをthrowします。

### JUNSONDecodable
**JUNSONDecodable**はデコード対象に実装するprotocolです。以下のようにdecodeメソッドを定義すると、
デコード時のJUNSONがAnyJUNSONで渡されるので、いずれかのJUNSONでデコード処理を記述して下さい。
以下のchildプロパティのように、JUNSONDecodableを実装していれば、子データ・モデルも含め簡単にデコードが可能です。

```swift
struct Person: JUNSONDecodable {
    let name: String
    let child: Person?
    
    static func decode(junson json: AnyJUNSON) -> Person? {
        let person = Person()
        person.name = json.asDefault["name"].decode()
        person.child = json.asOptional["child"].decode()
        return person
    }
}
```

また、デコードｔに失敗した場合にデフォルト値をセットし、Nonnullな形にしたい場合は別途**JUNSONDefaultValue**を記述し、デフォルト値を定義する必要があります。
Int,Int64などのデフォルト値は既に定義されている為、 [こちら](https://github.com/SatoshiN21/JUNSON/blob/master/JUNSON/Classes/JUNSONDecodable.swift)を参照下さい。

```swift
static var defaultValue: Person {
    return Person(name: "", child: nil)
}
```

## エンコードとJUNSONEncodable
JUNSONにはオブジェクトをエンコードする機能も存在します。
エンコード用の処理を実装することで
先程のPersonオブジェクトは以下のような形でエンコードが可能です。

```swift
let persons: [Person]!
..

let data = JUNSON.encode(any: persons)
print(String(data: data, encoding: .utf8)!)
// ↓
/*
[
    {
        "child": null, 
        "name": "Luke Skywalker"
    }, 
    {
        "child": {
            "child": null, 
            "name": "Luke Skywalker"
        }, 
        "name": "Darth Vader"
    }
]
*/
```

エンコード対象のclass/structは**必ずJUNSONEncodableを実装していなければなりません**。
上記のサンプルでは*Person*は以下のようにencode()を実装しています。
ここで返した構造通りにEncodableなオブジェクトをJSONにエンコードします。

```swift
func encode() -> Any? {
    return ["name":name,"child":child]
}
```

後は`JUNSON.encode(any:Any)` に渡すだけでよしなにJSONへエンコードしてくれます。
この時に渡せるものは**JUNSONEncodableを実装したオブジェクト**,**[String:JUNSONEncodable]**,**[JUNSONEncodable]**のいずれかを渡すことができます。
※注意点としては、rootObjectはDictionaryかArrayいずれかとして下さい。例えば `JUNSON.encode(any:"value")`はキー値が存在しない為エンコードができません。

エンコード時も各JUNSONに応じてエラー時のハンドリングを切り替える事ができます。

```swift
let data = JUNSON.encode(any:nil) // data == Data()

let data = OptionalJUNSON.encode(any:nil) // data == nil

do {
    let data = try TryJUNSON.encode(any:nil)
} catch let e {
    // e == JUNSONError.noEncodableObject
}
```

## まとめ
個人的に今まで他のJSONライブラリには
**もっといろんな方法でJSONを扱えればいいのになぁ**と感じていました。

例えば、単純にauthTokenをJSONから取り出したいだけなのに、わざわざデコーダを作ったり、または大げさなtry-Catchを用意してガチガチにハンドリングするのは気が引けます。
ただ一方で、プロダクトの中で多数のデータモデルが定義されている場合は
ある一つのプロトコルでデコード処理を記述する事で格段に見通しがよくなる事もあることも確かです。

シチュエーションやプロダクトの規模によって、様々な方法でJSONと触れ合えるようにこのライブラリを作成しました。
もしよければ、スター:star:を頂けると非常にありがたいです！！
また、PRやissueもお待ちしております。むせび喜びます。