JSON API(json:api)をSwiftでいい感じにシリアライズする

このエントリーをはてなブックマークに追加

このブログではどのようにしてSwiftを使ってJSON API(json:api)のレスポンスをシリアライズするかについて書いていきます。

http://jsonapi.org/

Swift Advent Calendar 2017の6日目の記事ですので、サーバサイド実装の詳細については触れません。

概要

  1. JSON APIについて
  2. JSON APIをシリアライズできるクライアント
  3. Spineを使ったJSON APIのシリアライズ
  4. SpineのSwift4対応、メンテナンス状況について
  5. まとめ

JSON APIについて

JSON APIはJSONを使ったAPIを作るための仕様です。レスポンスの返し方だけでなくリクエスト方法やエラーの表現等もしっかり定義されています。こちらには、JSON Schemaを使ってJSON APIを定義したファイルも置かれています。 もしサーバサイドがRailsだった場合はActiveModelSerializers(AMS)を使うと実装も簡単です。

JSON APIをシリアライズできるクライアント

jsonapi.orgには各言語で使えるクライアントの一覧が掲載されています。 JSON APIはググラビリティが低く、「APIでJSONを返す方法」といった情報が出てきてしまいがちなので、提供されている一覧から探すのと楽です。

iOS向けには以下の2つのクライアントが掲載されています。

クライアント 実装言語 Star数 最終コミット日
Spine Swift 261 2017-10-07
jsonapi-ios Objective-C 164 2016-06-16

(Star数は2017/12/04時点)

最終コミット日、Star数を見る限り、Swiftで使うならSpineを選ぶのが無難です。

Spineを使ったJSON APIのシリアライズ

カウルのiOSアプリでは実際にSpineを使っていますので、サンプルコードと共に使い方を紹介します。

ちなみにSpineは通信処理もできますが、シリアライズ処理だけを使うこともできます。

You can also just use the Serializer to (de)serialize to and from JSON:

ここでは通信部分を省略するので、実際に使う際はAlamofire等で取得したデータを渡してください。

シリアライズ対象のJSON

以下のJSONを処理する想定のコードで解説します。

{
    "data": [
        {
            "id": "1",
            "type": "rooms",
            "attributes": {
                "price": 111000000
            },
            "relationships": {
                "building": {
                    "data": {
                        "id": "1",
                        "type": "buildings"
                    }
                }
            }
        },
        {
            "id": "2",
            "type": "rooms",
            "attributes": {
                "price": 91000000
            },
            "relationships": {
                "building": {
                    "data": {
                        "id": "1",
                        "type": "buildings"
                    }
                }
            }
        }
    ],
    "included": [
        {
            "id": "1",
            "type": "buildings",
            "attributes": {
                "name": "カウルマンション",
                "full_address": "東京都渋谷区広尾1丁目7-7"
            }
        }
    ]
}

「1つのビルに2つの部屋が紐づいたデータ」です。 JSON APIではこのようにデータがフラットですし、relationshipsにより複数のオブジェクトから1つのオブジェクトへの参照が可能です。

他にもmetapaginationといった概念もあるのですが、詳細についてはフォーマットの仕様を参照してください。

シリアライズ後に使うstruct

シリアライズが完了した後に返すオブジェクトは以下のstructを返すことにします。

// Room.swift

struct Room {
    let id: Int
    let price: Int
    let building: Building
}

// Building.swift

struct Building {
    let id: Int
    let name: String
    let address: String
}

Resouceを使ったシリアライズに必要な記述

SpineResourceクラスを継承してシリアライズする際は以下のようにします。initが多いですが大事なのはresourceTypefieldsです。convertstructを返すためのサンプルです。

// Room.swift

import Spine

class RoomResource: Resource {

    // ここはオプショナルで定義しておく必要がありました。後でstructに変換する際にunwrapしてます。
    // NSNumberも気になるので取り回すstructではIntに変換します(´ω`)
    var price: NSNumber?
    var building: BuildingResource?

    required init() {
        super.init()
    }

    required init(coder: NSCoder) {
        super.init(coder: coder)
    }

    init(id: String) {
        super.init()
        self.id = id
    }

    override class var resourceType: ResourceType {
        return "rooms"
    }

    override class var fields: [Field] {
        return fieldsFromDictionary([
            "price": Attribute(),
            // ここにBuildingResourceとのリレーションを書きます。
            "building": ToOneRelationship(BuildingResource.self)
            ])
    }

    // structに変換する関数を持たせておきます。
    func convert() -> Room {
        return Room(
            id: id,
            price: Int(truncating: price),
            building: building
        )
    }

}

// Building.swift

import Spine

class BuildingResource: Resource {

    // ここはオプショナルで定義しておく必要がありました。後でstructに変換する際にunwrapしてます。
    var name: String?
    var address: String?

    required init() {
        super.init()
    }

    required init(coder: NSCoder) {
        super.init(coder: coder)
    }

    init(id: String) {
        super.init()
        self.id = id
    }

    override class var resourceType: ResourceType {
        return "buildings"
    }

    override class var fields: [Field] {
        return fieldsFromDictionary([
            "name": Attribute(),
            // JSON内の名前から変えたい場合はこんな感じ。スネークケースやキャメルケースから自動変換もできます。
            "address": Attribute().serializeAs("full_address")
            ])
    }

    // structに変換する関数を持たせておきます。
    func convert() -> Building {
        return Building(
            id: id,
            name: name,
            address: address
        )
    }

}

JSONをシリアライズ

ここまで用意したらシリアライザーを実際に使って先ほどのJSONを処理します。

let serializer = Serializer()
serializer.registerResource(BuildingResource.self)
serializer.registerResource(RoomResource.self)

// ここでシリアライズ。よしなにguardなど。
guard let document = try? serializer.deserializeData(data), let roomResources = document.data as? [RoomResource] else {
    return nil
}

var rooms = [Room]()

// structで詰め替えたければ。
for roomResource in roomResources {
    rooms.append(roomResource.convert())
}

シリアライズ後のstructの拡張

実際に使う際はEquatable対応等もしておくと比較やフィルターができて便利ですね。

extension Room: Equatable {

    // MARK: Equatable

    static func == (lhs: Room, rhs: Room) -> Bool {
        return lhs.id == rhs.id  &&
            lhs.price == rhs.price &&
            lhs.building == rhs.building
    }

}

extension Building: Equatable {

    // MARK: Equatable

    static func == (lhs: Building, rhs: Building) -> Bool {
        return lhs.id == rhs.id  &&
            lhs.name == rhs.name &&
            lhs.address == rhs.address
    }

}

SpineのSwift4対応、メンテナンス状況について

10月7日の時点でSpineの作者が、メンテナンス継続しない旨をコミットしています・・😢

この記事を書いている時点では使っている限り動作に問題なく、Swift4対応のPull Requestが出ています。(こちらにCIが通るようにしたブランチをおきました。)

直近でJSON APIをシリアライズする場合はSpineを使うことになるでしょうが、次のSwiftのバージョン更新時には対応が必要になるかもしれません。

まとめ

  • JSON APIはJSONを使ってAPIを作るための仕様
  • Swiftで処理するなら現状はSpine

SwiftでJSON APIを返すAPIを利用する際の参考になれば幸いです!

まっくす


Housmartでは不動産業界を変えるカウルを支えるエンジニアを募集しています。
今話題のReTech!業界を変えるカウルを支えるエンジニアをWanted!

このエントリーをはてなブックマークに追加