Edited at

Retrofit2.0に備えてKotlinで始めるMoshi(JSONパーサ)

More than 3 years have passed since last update.

Square製の軽量JSONパーサのMoshiをご存知でしょうか。今年の9月にv1.0がリリースされたところでまだ新しいライブラリですが、個人的に注目しております。

Androidの開発で以前まではGsonを使用していましたが、単純なJSONとPOJOの変換ならもっとシンプルで小さなライブラリがないかなと思っておりました。そこで出会ったのがMoshiです。

そして、現在開発が行われているRetofit2.0ではレスポンスとPOJOの変換の際に使用者がコンバータを選択できるようになり、Moshiも使用することもできます。

バージョンアップに合わせGsonからMoshiに移行しようと考えているため、基本的な使い方をおさえておきたと思い記事にしました。

合わせて、たろうさんがKotlinとJacksonで苦労したという記事を見て、Moshiは大丈夫か確認したかったので、Kotlinを使用しています。

ライブラリやメソッド数を他のJSONパーサと比較してくださっている記事もありました。ライブラリサイズとメソッド数がGsonより抑えられており、Androidで使うには嬉しいですね。

確認のために作成したサンプルはGitHubにあります。


Moshiの使い方


導入

プロジェクトに追加するにはGradleに依存関係を追加します。


build.gradle

compile 'com.squareup.moshi:moshi:1.0.0'



POJOの定義

以降で使用するクラスは次のようになります。

(スライドのよくあるクラスを参考にさせていただいております)

KotlinでPOJOを扱うならdata型にしたいので、プロパティはイミュータブルかつ、全てコンストラクタで設定するにしています。

(デバッグする際に#toString()をいい感じに用意してくれて便利でした)

フィールドにアノテーションをつけることもできますが、JSONのキーとプロパティ名が一致していれば、後はよしなにやってくれます。

// 記事を表すクラス

data class Article(
val id: String,
val title: String,
val author: Author
)
// 記事の作者を表すクラス
data class Author(
val id: String,
val name: String
)

JSONのキーとフィールド名を変わる場合などでは、@Json アノテーションを付けてやります。

// 記事を表すクラス 

data class NamedArticle(
@Json(name = "articleId") val id: String,
val title: String,
val author: NamedAuthor
)

// 記事の作者を表すクラス
data class NamedAuthor(
@Json(name = "authorId") val id: String,
val name: String
)

他にプリミティブ型やコレクションをデフォルとサポートしています。

Androidのクラスをフィールドに使いたい場合は、後述のアダプタを書く必要があります。


JSON <-> POJO変換

クラスとJSONの変換の流れは次のようになります。

// ① Moshiオブジェクトを生成

val moshi = Moshi.Builder().build()
// ② JSON<->POJO変換ためのアダプタ生成(変換対象のクラスを渡す)
val adapter = moshi.adapter(Article::class.java)

// ③JSONに変換するオブジェクトを生成
val article = Article(id = "10",
title = "MoshiMoshi",
author = Author(id = "1", name = "droibit"))
// ④ #toJson()でJSON文字列に変換
val json = adapter.toJson(article)
// ⑤ #fromJson()でJSON文字列からオブジェクトに変換
val restoreArticle = adapter.fromJson(json)

基本的な流れは、Moshiからクラス用のアダプタ作成して、JSONを放り込む用な感じです。上記の Article クラスから生成されたJSONは次のようになります。

{

"author": {
"id": "1",
"name": "droibit"
},
"id": "10",
"title": "MoshiMoshi"
}

Articleはdata型でプロパティはイミュータブルなクラスですが、JSONから変換ができました。

(Moshiの内部ではリフレクションでフィールド値へセットしているようです)


Custom Type Adapter

プリミティブ意外の型をPOJOに含めたい場合、カスタムアダプタを実装します。

data class Article(

val id: String,
val title: String,
val author: Author,
val link: Uri // ← New!!
)

class UriAdapter : JsonAdapter<Uri>() {

companion object {
// Moshiのソースコードではアダプタ毎にファクトリを用意していたのでそれに習う。
// 多分、この方法が手っ取り早いと思われます。
// Uriなどのクラスごとに一つのアダプタを作成しなくてもOK。いくつかまとめてもOK
val FACTORY: Factory = object: Factory {
override fun create(type: Type, annotations: Set<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (type === Uri::class.java) {
return UriAdapter()
}
return null
}
}
}

@Throws(IOException::class)
override fun fromJson(reader: JsonReader): Uri {
return Uri.parse(reader.nextString())
}

@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: Uri) {
writer.value(value.toString())
}

override fun toString(): String {
return "JsonAdapter(Uri)"
}
}

// Moshiオブジェクトを作成するときにファクトリを追加する。
val moshi = Moshi.Builder()
.add(UriAdapter.FACTORY)
.build()
// 後は同じ

ドキュメントにないので、ソースコードからプリミティブ型のアダプタを参考にしています。単純な変換ならこれでよさそうです。

詳しいところはこの辺りのクラスが参考になるかと思います。

また、JSONとPOJOが対になっていない場合、変換用のアダプタを追加することで上手く変換することができます。こちらは、Moshiのドキュメントがわかりやすいです。


フィールドの除外

変換の際に特定にフィールドを除外したい場合は、@Transient アノテーションを使用します。

(Javaの場合は transient 修飾子)

class Article {

@Transient var updated: String? = null
}

このようにアノテーションをつけるだけで自動的に除外してくれます。


Retrofit 2.0-beta2と連携

最後に、Retrofit2.0-beta2のJSONコンバータにMoshiを使用する方法です。


導入

RetrofitでMoshiを使うためのコンバータは予め用意されているので、依存関係を追加するだけになります。


build.gradle

compile "com.squareup.retrofit:retrofit:2.0.0-beta2"

compile "com.squareup.retrofit:converter-moshi:2.0.0-beta2"


実装

ちょくちょくお世話になっているlivedoor お天気Webサービス では、天気情報をJSONで取得することができるので、それにRetrofitを使ってみます。

POJOを定義する際に、プロパティが多くなってくる場合は、コメントで @property と書いておくと後々わかりやすいのではないかと思います。

/**

* @property location 予報を発表した地域
* @property title タイトル
* 以下略
*/
data class Weather(
val location: Location,
val title: String,
val link: Uri,
val publicTime: String,
val description: Description,
val forecasts: List<Forecast>,
val pinpointLocation: PinpointLocation,
val copyright: Copyright
)
// 以下略

地域ごとの番号をクエリに設定しGETリクエストすると天気情報が取得できます。

interface WeatherService {

@GET("/forecast/webservice/json/v1")
fun weather(@Query("city") city: String): Call<Weather>
}

あとはRetrofitの作法に則ってコールします。

val moshi = Moshi.Builder()

.add(UriAdapter.FACTORY)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("http://weather.livedoor.com")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
val service = retrofit.create(WeatherService::class.java)

try {
val response = service.weather("130010").execute()
val weather = response?.body()
// 何かする
} catch (e: Exception) {
// エラー処理
}

上記のような簡単な変換なら軽量なMoshiでも十分いけるのではと感じております。


注意点

JSONから上記のような Article クラスに変換する際、JSON側にキーが存在しないことがあると注意が必要です。

たとえば、Articletitle がJSONに存在しない場合です。

{

"author": {
"id": "1",
"name": "droibit"
},
"id": "10"
}

JSONからの変換は成功するのですが、生成された Article オブジェクトの title プロパティが null にっていることです。

コンストラクタの宣言では、オプショナル型にしていないにもかかわらず、値は null になります。(リフレクションを使っている関係かと思われます)

そのため、非オプショナル型の値として扱っているとヌルポが出る可能性があるのです! Kotlinで!

そのため、String型の値などにはデフォルト値を設定しておくといいでしょう。

data class Article(

val id: String = "",
val title: String = "",
val author: Author
)

以上、簡単ですがMoshiの使い方でした。


Written with StackEdit.