Tryswift anat gilboa facebook

SwiftにおけるJSONデコーディングについて詳しく調べましょう

Swiftの静的型付けの特性がシリアライズされたオブジェクトのデコードを難しくしています。 代替案としてカリー化のような特性を用いるのはおもしろい試みです。この try! Swift NYC の講演では、 Anat Gilboa がSwiftの関数型の側面を利用して、JSONのパースを楽しく、エキサイティングなものにしてくれます!


イントロダクション (0:00)

JSONのパースについてお話します。元々この講演のインスピレーションを得たのは、チームメイトの一人がネットワーク層をObjective-CからSwiftに移行するという案を提案したことでした。その案には2つのサードパーティ製ライブラリの利用が含まれていました。ArgoResult です。JSONのデコードと、それぞれのエラーハンドリングを行うためです。JSONをデコードする経験があまりありませんでしたが、いくつかのREADMEやXcodeのプロジェクトをチェックすることにしたのです。

提案 (0:41)

Argo は、安全な方法でJSONからモデルを抽出するJSONデコードライブラリです。 Swiftの型システムを使用して、必要なデータフィールドを明示的に検証し、成功か失敗かを教えてくれます。

カリー化がコードで何をしているかよくわかっていなかったので、チェックしてみることにしました。「カリー化」という単語に馴染みのない方は、必要な引数よりも少ない引数の関数を与え、残った引数をとる関数を返すアクションのことだと思ってください。なので、Userオブジェクトは下記のようになります。


struct User {
	let id: Int
	let name: String
	let age: Int
}

static func create(id: Int)(name: String)(age: Int) -> User {
	return User(id: id, name: name, age: age)
}

引数はそれぞれのカッコにより分けられています。

Result はValueとErrorを、それぞれSuccessとFailureにラップできるマイクロフレームワークです。ここに例がありますが、JSONのエラーがあり、ErrorTypeenumで、2つのケースがあります。それから与えられたKeyを使って、JSONのDictionaryからStringの取得を試みている関数があります。


Result<Value, Error>

typealias JSONObject = [String:AnyObject]

enum JSONError : ErrorType {
	case NoSuchKey(String)
	case TypeMismatch
}

func stringForKey(json: JSONObject, key: String) -> Result<String, JSONError> {
	guard let value = json[key] else {
		return .Failure(.NoSuchKey(key))
	}
	if let value = value as? String {
		return .Success(value)
	}
	else {
		return .Failure(.TypeMismatch)
	}
}

記事の更新情報を受け取る

AnyObjectを返すのではなく、キーに対するStringの値と、エラー詳細のあるエラー型を含んだResultを返しています。キーに対応する値がないときには、guard letによって失敗させたり、値のアンラップに成功したり、型の不一致によって失敗させることができます。これらはエラー型から示される2つのエラーに過ぎません。

なぜSwift 2で導入されたthrowを使わずにこのようなことをするのか、疑問に思っていました。

SwiftでJSONをパースする (2:45)

SwiftでJSONをパースする際に起こる問題は何でしょうか?Swiftは静的型付け言語なので、型のある変数にオブジェクトを投げることができず、Objective-Cでやっていたように、コンパイラは型が宣言されたものであると信じています。今コンパイラはランタイムエラーが起こらないことを確認しています。これはすばらしいです。ですが、やることがもっとあることを意味しています。

なので、こんなユーザーモデルがあるとします。


struct User {
	let id: Int
	let name: String
	let age: Int
}

Swift 2以前では、こんな風になっていたでしょう。


let user: User?
if let user = json["user"] as? [String: String] {
	if let id = user["id"] {
		if let name = user["name"] {
			if let age = user["age"] {
				user = User(id, name: name, age: age)
			}
		}
	}
}

基本的なモデルオブジェクトを見ると、ユーザオブジェクトを作成するために if letのネストが連続しています。プロパティが多くなって、かなり煩雑で、冗長的です。そして美しくありません。

また、すべてのプロパティを取得しないとどうなりますか?何も起こりません。もっと良い方法があるのでしょうか? Swift2では、guard文が導入されて、ちょっとよくなりました。


let user: User?
guard let user = json["user"] as? [String: String] {
	if let id = user["id"],
			name = user["name"],
			age = user["age"] {
				user = User(id, name: name, age: age)
			}
	} else {
	return nil
}

これらの条件を満たしたときのみ、Userオブジェクトがつくられます。しかし繰り返しますが、エラーハンドリング部分ではあまり進んでいません。

do-catchはどうでしょうか? do-catchでは、doブロックでエラーを処理でき、エラーが発生したかどうかを判断するために catchで句にマッチします。これは順番に複数のtryに使えますが、do-catchを実行している関数がthrowしていることを確認する必要もあります。

このことから何がわかるでしょうか?同じ問題を他の人がどのように解決しようとしているかを知りたかったので、GitHubで人気のあるライブラリを分析することにしました。プロジェクトの中を見て、基礎を成している実装が何であったかを知りたいと思っていました。APIの単純さについては心配していませんでした。なぜなら、単に外部向けのものだからです。実際にJSONをどのように解析しているのでしょうか?

議題 (5:02)

いくつかの議題があります。例を通してみてみましょう。


import SwiftyJSON

struct User {
	let id: Int
	let name: String
	let age: Int
}

let json = JSON(["id":2378, "name":"Jack", "age": 23])
if let name = json["name"].string {
	// Do a thing
} else {
	print(json["name"].error)
}

SwiftyJSON でデコードする例です。nameを得るためにif-letを用いており、.stringというシンタックスで値を取り出しています。とても簡単ですね。美しいです。コードブロックの中でユーザオブジェクトのnameというプロパティを初期化できますし、elseでエラーを表示しています。


public let ErrorUnsupportedType: Int = 999
public let ErrorIndexOutOfBounds: Int = 900
public let ErrorWrongType: Int = 901
public let ErrorNotExist: Int = 500
public let ErrorInvalidJSON: Int = 490

SwiftyJSON.swiftファイルの先頭に独自のエラーが定義されています。多くのケースに対応しているわけではありませんが、型の不一致、間違ったエラー、存在していない、不正なJSONなど、実際のエラーに対応しています。enumで定義されていないのは面白いと思いました。なぜそのようになっているのかはわかりませんが、基本的にはただのエラーコードということです。


import Mapper

struct User {
	...
}
extension User : Mappable {
	init(map: Mapper) throws {
		try id = map.from("id")
		try name = map.from("name")
		age = map.optionalFrom("age")
	}
}

Mapper はLyftの人々が作成したJSONデシリアライズライブラリです。オブジェクトにthrowするイニシャライザがあり、値をセットします。内部的には、Mapper.swiftfromというフィールド関数があり、JSONを得ます。これがフィールドが空かどうかをチェックして、値をセットします。フィールドが空なら、MapperErrorをthrowします。


public enum MapperError: ErrorType {
	case ConvertibleError(value: AnyObject?, type: Any.Type)
	case CustomError(field: String?, message: String)
	case InvalidRawValueError(field: String, value: Any, type: Any.Type)
	case MissingFieldError(field: String)
	case TypeMismatchError(field: String, value: AnyObject, type: Any.Type)
}

enumです。ここで初めて登場しました。これらはMapperError.swiftファイルに宣言されており、throwしてenumでそれぞれのErrorTypeから値を抽出できるので、かっこいいと思いました。


import Freddy

struct User {
	...
}
extension User: JSONDecodable {
	public init(json value: JSON) throws {
		id = try value.int("id")
		name = try value.string("name")
		age = try value.int("age")
	}
}

Freddy も有名なライブラリです。オブジェクトのextensionを作り、JSONDecodableプロトコルに適合させます。これはJSONインスタンスからオブジェクトのインスタンスを作ります。ほとんどのプリミティブ型を処理し、initで実装されているすべてのextensionを持っています。Stringやint、bool,あなたが命名したものを含みます。内部のJSON initからのthrowのエラーは、変換可能な値ではなく、かなり説明的なメッセージを吐き出します。


public enum Error: ErrorType {
	case IndexOutOfBounds(index: Swift.Int)
	case KeyNotFound(key: Swift.String)
	case UnexpectedSubscript(type: JSONPathType.Type)
	case ValueNotConvertible(value: JSON, to: Any.Type)
}

またです。ErrorTypeがenumになっています。これをdo-catchで行うことが推奨されています。なのでモデルのinitでなくてもいいのです。モデルのinitからこれを行わない場合は、initのextensionがあります。

もうひとつライブラリがあります。Decodableです。


import Decodable

struct User {
	...
}
extension User: Decodable {
	static func decode(j: Any) throws -> User {
		return try User(
			id: j => "id",
			name: j => "name",
			age: j => "age"
		)
	}
}

Decodableプロトコル、decodeを実装し、Userオブジェクトのextensionを再度作成します。Decodable.swiftに宣言されたプロトコルがあるからです。さっき見たように、プリミティブ型のextensionにdecodeを追加しています。


public enum DecodingError: ErrorProtocol, Equatable {
	case typeMismatch(expected: Any.Type, actual: Any.Type, Metadata)
	case missingKey(String, Metadata)
	case rawRepresentableInitializationError(rawValue: Any, Metadata)
	case other(ErrorProtocol, Metadata)
}

抽出する人へすてきなことがあります。何を期待して、何をthrowするのかです。これはREADMEにありました。型にあるそれぞれのenumで、どのように実装するのかを見るのはかなり面白そうだと思いました。

発見したこと(9:00)

この発見したことリストを一部ご紹介したいと思います。これらのライブラリ間の相違点や類似点に気付かれましたら幸いです。

一般的に、throwするプロトコルがあります。これは、インスタンスのイニシャライザと指定されたJSONオブジェクトの上にあり、JSONの値から派生した何らかのエラーをthrowします。DecodableSyllable、JSONDecodable、JSONConvertibleという名前になっていると思います。

もう1つ興味深いのは、オブジェクトのプロパティをマップするさまざまな方法です。値を実際にどのようにデコードしていますか?カスタム演算子をオーバーライドするライブラリがあります。それは演算子を過負荷にするでしょう。他のライブラリはSwiftyJSONのように、その型の添字を作成するだけです。演算子をオーバーロードするライブラリは、マップされたオブジェクト上のプロパティを抽出していることがわかりました。最初から圧倒的に見えるかもしれませんが、ドキュメントでは、概要と、オプショナル、非オプショナル、配列、辞書のために何をしていたのかをよく説明されていました。ライブラリで多くの演算子を見ましたが、狂ってました。日々の仕事で頻繁に使う演算子に負荷をかけることはありません。だから、人々が何を思いついたかを見るのは面白かったです。

添字 JSONについて最初に学んだとき、辞書に入れ子になっているアイテムのように見えて、値を抽出するためにオブジェクトにインデックスを付けることが望ましいと思いました。とても直感的でした。しかし、私がJSONのデコードを始めたとき、それを行うことはできず、SwiftyJSONは、シームレスに行っており、本当にきれいだと思いました。彼らは基本的な実装のカスタム添え字をサポートしています。

エラー処理 どのような場合でも、何らかの形で結果を処理する必要があります。 1つの方法は、Resultプロトコルを実装し、成功または失敗に伴ってthrowされたエラー型をラップすることです。他のライブラリは、 doでアンラップして、大文字と小文字を区別したカスタムエラーをcatchすることを推奨しています。これは、ほとんどのライブラリが、これまでみたように、それぞれの型に対してこれらのカスタムエラーを持ち、独自のエラー処理を実装するのに役立ちます。しかし、いくつかの安全性を追加したい場合は、別の失敗または成功でその列挙型のエラー型で定義されているエラーをラップすることができます。次に、これまで見てきたライブラリのいくつかのエラー型を見てみましょう。

まとめ (12:03)

結局のところ私は多くのライブラリが好きでしたが、チームは解決策を選ぶための特定の基準を持っていました。

主な違いの1つは、型チェックとそれぞれのエラー処理です。どのような選択をしても、ランタイムエラーを防ぐ方法でJSONを受け取るのに、コンパイラが役立つ型安全なソリューションがあることを確認するソリューションが必要になります。SuccessとFailureに対するFailureのケースをラップしたResultと同じように、宣言的なエラー処理を追加することもできます。

ジェネリックが使えるものであることを望んでいるでしょう。多くのライブラリが、プリミティブ型の場合、intとboolとstringをデコードすることを見ました。これは、ジェネリック関数を持つのと同様です。他の人は自分自身のint、extension、initsを持っていて、私も興味深いと思っていました。

JSONをデコードするための宣言的で型安全な方法を探しているなら、ArgoとResultを使うのが良い選択かもしれません。 Argoの使用例を次に示します。


import Argo
import Curry
import Result

struct User {
	let id: Int
	let name: String
	let age: Int?
}
extension User: Decodable {
	static func decode(j: JSON) -> Decoded<User> {
		return curry(User.init)
			<^> j <| "id"
			<*> j <| "name"
			<*> j <|? "age"
	}
}

デコードを行うDecodableプロトコルに適合したUserオブジェクトが得られました。それをJSONに渡すと、curryになります。繰り返しますが、最初にみたカリー化と同じではありません。Swift 3では廃止予定です。Argoのカリー化の基礎となる実装がそうでないことを確認してください。

idnameageを抽出することができます。これらのプロパティを取得しなければ失敗し、それぞれint、string、intにマップされます。

このトピックを提供してくれたコミュニティメンバーに感謝します。私があまり精通していたものではなかったので、たくさんの疑問がありました。質問を返してくれたり、プルリクエストを承認してくれたチームメンバーに感謝します。JSONデコードライブラリにコントリビュートしてくださった皆様、ありがとうございます。

About the content

2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。

Anat Gilboa

Anat is a software engineer at American Express, where she enjoys bringing the delight of Swift into the CoreMobile codebase daily. She is a Cocoa-turned-CocoaTouch developer with her initial start in localization automation tools. Prior to American Express, she studied Computer Science and Mathematics at the University of Virginia, where she found her love for applying ML to Genre Classification. In her free time, Anat likes to slackline and play ultimate frisbee.

4 design patterns for a RESTless mobile integration »

close