SwiftでJSONのマッピングにはUnboxが便利らしい - Re.Ra.Ku アドベントカレンダー day 7
Re.Ra.Ku アドベントカレンダー 7日目です。
こんにちは、Re.Ra.Kuさんのパートナーとして開発に携わっています、神場です。 アドベントカレンダーをやるということで、せっかくならと思い手を挙げたところ参加出来ることになりましたので、書かせていただきます!
概要
今回はSwift製のJSON decoderであるUnbox
をプロダクトで採用してみて、かなり使いやすいということが分かりましたので、いくつか使い方のサンプルを紹介するという内容の記事になっています。
Unboxを選択した理由
Swift3でJSONを扱うライブラリにはSwiftyJSONやArgoがあると思いますが、今取り掛かっているプロダクトでは
- マッピング時に型推論が出来る
- 異なる型への変換が楽に出来る
- インターフェースが直感的である
- 実装が軽い
- Githubのスター数がある程度(300以上ぐらい?)付いている
等の理由からUnboxを採用しました。
この記事で紹介する内容
この記事では
- 基本的な使い方
- unboxのネストの仕方
- 異なる型への変換(
format
,transform
) - DTO(Data Transfer Object)を使わないマッピング(
performCustomUnboxing
)
の流れで使い方を紹介したいと思います。
なお、日本語を読むのが億劫な人向けに、先にコードの全文へのURLを貼っておきます。
基本的な使い方
Unboxの最も基本的な使い方としては、マッピングしたいclassもしくはstructにUnboxable
プロトコルを実装するだけです。例えば
{ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "ユーザー1" }
というJSONを
struct User { let id: String let name: String }
にマッピングしたい場合は
extension User: Unboxable { init(unboxer: Unboxer) throws { self.id = try unboxer.unbox(key: "id") self.name = try unboxer.unbox(key: "name") } }
のように書いておきます。実際に使うときは
let user: User = try unbox(dictionary: dictionary)
もしくは
let user: User = try unbox(data: data)
のようになります。この記事やサンプルコードではdictionary([String : Any]
のエイリアス)を使う方で統一してあります。
ネストの仕方
マッピングしたいオブジェクトがネストする場合は、ネストしている子のほうにもマッピングを定義するだけで大丈夫です。
{ "group": { "name": "グループ1", "users": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "ユーザー1" }, { "id": "b078bb12-7616-4207-9f16-6a991c7ae16a", "name": "ユーザー1" } ] } }
のようなJSONに対して、さきほどのUser
の配列を持つ
struct Group { let name: String let users: [User] }
というstructへのマッピングを考えると、Group
に対して
extension Group: Unboxable { init(unboxer: Unboxer) throws { self.name = try unboxer.unbox(keyPath: "group.name") self.users = try unboxer.unbox(keyPath: "group.users") } }
のようにしてやれば、
let group: Group = try unbox(dictionary: dictionary)
を実行する際に、users
のマッピングでUserのinit(unboxer: Unboxer)
が呼び出されるようになります。
(JSONのほうがネストしている場合は上記のようにkeyPath
で直接取得することが出来ます。)
異なる型への変換
マッピング時に、String -> UserId
のように特定の型へ変換したい時の処理です。これには二通りの方法があります。
ここでは、先ほどのUser
のIDを独自の型であるUserId
へマッピングする例を考えましょう。コードにすると
struct UserId { let value: UUID } struct User { let id: UserId let name: String }
のような感じです。(サンプルなので実態は単なるUUIDですが。。)
unbox
時にformatter
を渡す
こちらはunbox
時に明示的に変換するformatter
を渡してやる方法です。UnboxFormatter
を実装したクラス
class UserIdFormatter: UnboxFormatter { typealias UnboxRawValue = String typealias UnboxFormattedType = UserId func format(unboxedValue: UnboxRawValue) -> UnboxFormattedType? { guard let uuid = UUID(uuidString: unboxedValue) else { return nil } return UserId(value: uuid) } }
を用意して
self.id = try unboxer.unbox(key: "id", formatter: UserIdFormatter())
のように渡します。
DTO(Data Transfer Object)自体にtransform(UnboxableByTransform
プロトコル)を実装する
さきほどはformatter
をunbox
時に明示的に渡していましたが、こちらはunbox
時にマッピングしたい先のDTOに定義しておいた変換が暗黙的に呼ばれる方法です。DTO自体にUnboxableByTransform
を実装します。
extension UserId: UnboxableByTransform { typealias UnboxRawValue = String static func transform(unboxedValue: UnboxRawValue) -> UserId? { guard let uuid = UUID(uuidString: unboxedValue) else { return nil } return UserId(value: uuid) } }
self.id = try unboxer.unbox(key: "id")
DTOを使わないマッピング
上のサンプルはDTOにextensionでマッピングを定義していきましたが、performCustomUnboxing
を使うことで直接クロージャーとしてマッピングを渡すことも出来ます。単純な例として
{ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "ユーザー1" }
というJSONであれば
func printIdAndName(dictionary: UnboxableDictionary) throws { let pair: (UserId, String) = try Unboxer.performCustomUnboxing(dictionary: dictionary, closure: { unboxer in let id: UserId = try unboxer.unbox(key: "id", formatter: UserIdFormatter()) let name: String = try unboxer.unbox(key: "name") return (id, name) }) print(pair) }
のようにしてid
とname
を取り出すことが出来ます。
ネストする場合はunbox
したものをさらにperformCustomUnboxing
のクロージャーでunbox
していく形になります。(長いのでコードの全文を参照)
まとめ
どうでしょうか。Unboxの機能自体はそこまで多くありませんが、以上の程度のことが出来ればプロダクションでも十分使えるかと思います。JSONのマッピングで消耗している方はUnboxの導入を検討していてはいかがでしょうか。ここで紹介した以外にもEnumへのマッピング等他の機能もありますので、気になる方は公式ドキュメントのほうをご覧ください。
余談
try
を多用するのはあまりスマートでない気もしますが、かといってイニシャライザをオプショナルにして全てにif let
やguard let
などを付けるのも冗長なので、この辺りを割り切ったのがUnboxなのかなという印象です。冒頭でちょっと触れたArgoでは(Curryを使って)マッピング用のイニシャライザをカリー化して巧妙に回避しているので、もし機会があれば検証したいです。