LoginSignup
8
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-06

JUNSON

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

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

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

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

作ろうと思った背景

社内のハイパーiOSエンジニアが唐突に

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

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

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

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

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

どんな事ができるの?

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

star_wars.json
{
    "gender": "male", 
    "height": 1.72, 
    "name": "Luke Skywalker"
}

このJSONをデータ・モデルとして定義した以下の構造体にデコードします。 JUNSONDecodableJUNSONDefaultValue については後ほど説明します。

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に取り込めばデコード可能です

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はJUNSONDecodableJUNSONEncodableの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
    • Decodableなvalueをその他の形式に変換する(例えばStringを受け取ってDateに変換)

3種類のJUNSON

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

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

JUNSON(デフォルト値)

let junson = JUNSON(string:string)
let authToken: String = junson["response"].decode(key:"auth_token")

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

let authToken: String = junson["response"]["auth_token"].decode()

OptionalJUNSON(Optional)

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

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

TryJUNSON(try-Catch)

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を実装していれば、子データ・モデルも含め簡単にデコードが可能です。

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
    }
}

また、デコードtに失敗した場合にデフォルト値をセットし、Nonnullな形にしたい場合は別途JUNSONDefaultValueを記述し、デフォルト値を定義する必要があります。
Int,Int64などのデフォルト値は既に定義されている為、 こちらを参照下さい。

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

エンコードとJUNSONEncodable

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

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にエンコードします。

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に応じてエラー時のハンドリングを切り替える事ができます。

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もお待ちしております。むせび喜びます。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9