[Swift] タイプセーフなJSONデコーダ「Himotoki」を試してみた

2015.12.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

(1) Himotokiとは

Himotokiは(紐解き)は、Syo Ikeda氏によって作成されたタイプセーフなJSONのデコードライブラリです。

https://github.com/ikesyo/Himotoki

特徴として、以下のようなものが挙げられます。

  • Swiftのみで書かれている
  • 依存ライブラリがない
  • JSONのデコードに特化されている
  • 簡潔にモデル定義が可能
  • タイプセーフ
  • 非常に軽量(Sourceの下で21,487byte)
  • 国産

動作環境は、次のとおりとなっています。

  • Swift 2.1 / Xcode 7.2
  • OS X 10.9 or later
  • iOS 8.0 or later (by Carthage or CocoaPods) / iOS 7 (by copying the source files directly)
  • tvOS 9.0 or later
  • watchOS 2.0 or later

(2) インストール

Himotokiは、CocoaPodsでインストール可能になっています。

use_frameworks!
pod "Himotoki", "~> 1.3"

依存関係はなく、Himotokiのみがダウンロードされます。

$ pod install
Updating local specs repositories
Analyzing dependencies
Downloading dependencies
Using Himotoki (1.3.2)
Generating Pods project
・・・省略・・・
$

(3) Decodableプロトコル

Himotokiの基本は、下記のプロトタイプです。デコードしたいデータ型を、このプロトタイプに従って定義するだけです。

なお、必須作業は、decodeというメソッドの実装のみです。

“Decodable.swift”

public protocol Decodable {
    typealias DecodedType = Self

    /// - Throws: DecodeError
    static func decode(e: Extractor) throws -> DecodedType
}

それでは、実装について確認してみましょう。

2 Decodableプロトコルの実装

(1) 基本型

最初に、Decodableプロトタイプの基本的な実装について見ていきます。

例として、次のようなJSON形式のデータをデコードの対象とします。

{
    “name”:”taro”,
    “age”:22
}

Decodableプロトタイプで必須となるdecodeメソッドでは、プロパティの初期化を行います。この際、パラメータのExtractorからは、演算子( <| )を利用してキーに指定した値を受け取れますが、この時点で型の検査が行われており、値の取得に成功した時点でタイプセーフになっています。 [swift highlight="6"] struct Person : Decodable{ let name:String let age:Int static func decode(e: Extractor) throws -> Person { return try Person(name: e <| "name", age: e <| "age") } } [/swift] build()を使用すると、プロパティを定義した順番に揃えるだけで初期化できます(プロパティ名も省略可能)。そして、この方法が最も簡潔な記法となります。 なお、build(_:)(...)は、1つ目の括弧に自分自身(ここでは、Person)を返すstaticメソッド若しくは、initializerを与え、2つ目の括弧にはその引数を渡します。そして、2つ目の括弧内の各引数は1つ目のメソッドから型推論されます。 [swift highlight="6"] struct Person : Decodable{ let name:String let age:Int static func decode(e: Extractor) throws -> Person { return try build(self.init)( e <| "name" , e <| "age") } } [/swift] 上記を検証すると次のようになります。 [swift] let json: [String: AnyObject] = [ "name": "taro", "age": 22 ] let person: Person? = try? decode(json) if( person != nil){ print("name:\(person!.name) age:\(person!.age)") //name:taro age:22 }else{ print("error") } [/swift]

(2) 条件分岐を含むデコード

単純にデコードするだけなく、JSONの内容によって条件分岐などを行いたい場合は、次のように、一旦変数で受けてから処理することになります。

なお、この場合、先に説明したように、値を取り出す時点で検査が行われますので、変数の型は明示する必要があります。

struct Person : Decodable{
    let name:String
    let age:Int
    
    static func decode(e: Extractor) throws -> Person {
        let a:Int = try e <| "age"  // Int型としてデコードする
        let n:String = try e <| "name" // String型としてデコードする
        if( a > 20 ){
            // 条件による処理など
        }
        return Person(name: n , age: a)
    }
}

また、init()を定義して、その中で処理してもいいでしょう。

struct Person : Decodable{
    let name:String
    let age:Int
    
    static func decode(e: Extractor) throws -> Person {
        return try Person(_name: e <| "name", _age: e <| "age")
    }
    init(_name:String,_age:Int){
        if( _age > 20 ){
            // 条件による処理など
        }
        name = _name
        age = _age
    }
}

3 演算子

ここまで、Extractorからの値の抽出に <| 演算子だけを使用してきましたが、Himotokiでは、この他にも、次のような6種類の演算子が使用可能です。

<| T A value
<|? T? An optional value
<|| [T] An array of values
<||? [T]? An optional array of values
<|-| [String: T] A dictionary of values
<|-|? [String: T]? An optional dictionary of values

それでは、各種の演算子を使用した例を確認してみます。

(1) キーが存在しない場合

対象のキーが存在しない可能性がある場合は、オプショナル型で定義します。

struct Person : Decodable{
    let name:String? // オプショナル型で定義する
    
    static func decode(e: Extractor) throws -> Person {
        return try build(self.init)( e <|? ["name"])  // 演算子は <|? を使用する
    }
}

実行例です。例外とならず処理できていることが確認できます。

let json: [String: AnyObject] = [:] // nameキーが存在しない
let person: Person? = try? decode(json)
print("name:\(person!.name)") // name:nil

(2) 階層のある場合

{
    “name”:{
        “first”:”taro”,
        “last”:”yamada”,
    }
}

階層下のキーをデコードしたい場合は、キーをString配列で指定します。

struct Person : Decodable{
    let firstName:String
    static func decode(e: Extractor) throws -> Person {
        return try build(self.init)( e <| ["name","first"]) // [“name”,”first”]で階層を指定する
    }
}
let json: [String: AnyObject] = [ "name":["last":"yamada","first":"taro"]  ]
let person: Person? = try? decode(json)
print("firstName:\(person!.firstName)")  // firstName:taro

(3) 辞書(Dictionary)

上のデータと同じですが、Dictionary型で読み込むことも可能です。

{
    “name”:{
        “first”:”taro”,
        “last”:”yamada”,
    }
}

プロパティをDictionaryで定義し、 <|-| 演算子で流し込みます。 [swift] struct Person : Decodable{ let name:[String:String] // Dictionary static func decode(e: Extractor) throws -> Person { return try build(self.init)( e <|-| ["name"]) // <|-| 演算子で読み込む } } [/swift] [swift] let json: [String: AnyObject] = [ "name":["last":"yamada","first":"taro"] ] let person: Person? = try? decode(json) print("name:\(person!.name)") // name:["last": "yamada", "first": "taro"] [/swift]

(4) 配列

{
    “sports”:[
        “football”,
        “swimming”,
        “baseball”
    ]
}

配列は、<|| 演算子で受け取ります。 [swift] struct Person : Decodable{ let sports:[String] // 配列を定義する static func decode(e: Extractor) throws -> Person { return try build(self.init)(e <|| ["sports"]) // 演算子は <|| を使用する } } [/swift] [swift] let json: [String: AnyObject] = [ "sports":["football","swimming","baseball"] ] let person: Person? = try? decode(json) print("sports:\(person!.sports)") // sports:["football", "swimming", "baseball"] [/swift]

(5) 型変換

当然のことですが、すべての型について検査やキャストが可能なわけではありません。

コードを確認すると、現時点では、すべての数値型(Int,Int8,Int16,Int32,Int62,UInt,UInt8,UInt16,UInt32,UInt62,Float,Double)及び真偽値(Bool)、文字列、及びNSNUmberについては、自動的にキャストできるようです。(StandardLib.swift 及び NSNumber.swift より)

そして、それ以外の型に対応する場合は、decodeメソッドの中で変換してやる必要があります。

struct Person : Decodable{
    let birthday:NSDate // Himotokiで未対応の型のプロパティ
    
    static func decode(e: Extractor) throws -> Person {
        
        let formatter = NSDateFormatter()
        formatter.dateFormat = "yyyy/MM/ddZ"
        let dt:NSDate = formatter.dateFromString(try e <| "birthday")! // 一旦、対応しているStringで受けてからNSDateに変換する
        
        return Person(birthday: dt)
    }
}
let json: [String: AnyObject] = [ "birthday": "1990-12-01Z" ]
let person: Person? = try? decode(json)
print("birthday:\(person!.birthday)") //birthday:1990-12-01 00:00:00 +0000

https://github.com/ikesyo/Himotoki/blob/master/Sources/StandardLib.swift https://github.com/ikesyo/Himotoki/blob/master/Sources/NSNumber.swift

(6) ネストしたデータ構造

次のように、配列となったクラスをデコードしたい場合は、2段階でクラスを設計します。そして、どちらのクラスもDecodableプロトコルに準拠させます。

{
    persopns:[
        {
            “name”:”taro”,
            “age”:22
         },
        {
             “name”:”hanako”,
             “age”:21
         }
   ]
}

上のJSONを処理するため、当初、配列内のデータ構造を読み込むためのPersonクラスを設計しています。そして、このPersonクラスの配列を保持するPersonsクラスを作成しています。

struct Person : Decodable{
    let name:String
    let age:Int
    static func decode(e: Extractor) throws -> Person {
        return try build(self.init)(e <| "name",e<|"age") 
    }
}
struct  Persons:Decodable{
    let persons:[Person] // 上記のPersonクラスの配列をプロパティとして持つ
    static func decode(e: Extractor) throws -> Persons {
        return try build(self.init)( e <|| ["persons"]) // キー"persons"を配列で読み込む
    }
}
        let json: [String: AnyObject] = [ "persons":
            [
                ["name":"taro","age":22],
                ["name":"hanako","age":21]
            ]
        ]
        let persons: Persons? = try? decode(json)
        
        for p in persons!.persons {
            print("person:\(p)")
        }
        //person:Person(name: "taro", age: 22)
        //person:Person(name: "hanako", age: 21)

4 まとめ

今回は、Himotokiについて動作を確認してみました。演算子の利用方法について慣れてくると、非常に快適に利用することができます。

こちらも、先日紹介させて頂いたAPIKit (タイプセーフな軽量HTTPクライアント) と同じく、Swiftのタイプセーフの長所を十分に引き出しているライブラリになっていると感じました。

5 参考リンク


https://github.com/ishkawa/APIKit
ObjectMapperでJSONマッピング
APIKit (タイプセーフな軽量HTTPクライアント)