Moshi を使って Java オブジェクトと JSON を相互変換する

# 概要
Java のオブジェクトを JSON に変換してファイルに保存したり、保存した JSON を Java のオブジェクトに変換するという処理を実装するのに、 Jackson を使うまでもないが Android SDK 標準の org.json を使うのは少々つらいという状況で、何か良いライブラリはないかと探したところ、 [Square, Inc.](https://squareup.com/) の [Moshi](https://github.com/square/moshi/) というライブラリがあると知ったので、簡単に試してみました。

----

# Moshi
[Moshi](https://github.com/square/moshi/) は Square Inc. が開発した JSON ライブラリです。Droidcon Montreal 2015 で Ok シリーズの1つとして紹介されていたようです。

[A Few 'Ok' Libraries (Droidcon MTL 2015)](https://speakerdeck.com/jakewharton/a-few-ok-libraries-droidcon-mtl-2015)

今回は触れませんが、Ok シリーズではほかに HTTP クライアントの [OkHttp](https://github.com/square/okhttp/) や、I/O ライブラリの [Okio](https://github.com/square/okio/) 等がリリースされています。

## バージョン
この記事を書いている 2017/08/12 現在では、最新のリリースは [1.5.0](https://github.com/square/moshi/tree/moshi-parent-1.5.0) (2017/05/15)です。

## ファイルサイズ
ファイルサイズが 64KB と小さいです。これが Android アプリ開発ではメリットになりえます。
Jackson が 1MB 近く、Gson も最新の 2.8.1 が 144KB であることを考えると、結構小さいです。

[1.4.0 の API ドキュメント](http://square.github.io/moshi/1.x/moshi/) によると、本体に含まれるクラスは下記の通りです。

1. [FromJson](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/FromJson.html)
1. [Json](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/Json.html)
1. [JsonAdapter](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonAdapter.html)
1. [JsonAdapter.Factory](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonAdapter.Factory.html)
1. [JsonDataException](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonDataException.html)
1. [JsonEncodingException](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonEncodingException.html)
1. [JsonQualifier](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonQualifier.html)
1. [JsonReader](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonReader.html)
1. [JsonReader.Options](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonReader.Options.html)
1. [JsonReader.Token](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonReader.Token.html)
1. [JsonWriter](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/JsonWriter.html)
1. [Moshi](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/Moshi.html)
1. [Moshi.Builder](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/Moshi.Builder.html)
1. [ToJson](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/ToJson.html)
1. [Types](http://square.github.io/moshi/1.x/moshi/com/squareup/moshi/Types.html)


## 依存
[GitHub リポジトリ](https://github.com/square/moshi)のソースコードを軽く見た程度だと、test の方で [android.util.Pair](https://developer.android.com/reference/android/util/Pair.html) を使っている程度で、ライブラリの実装内では Android SDK の依存が含まれていないようです。そのため、他の Ok シリーズ同様に Android アプリ以外でも使うことが可能です。

なお、[Okio](https://github.com/square/okio/) が依存に含まれています。こちらも Android SDK の依存は持たない I/O ライブラリです。

## ライセンス
1.5.0 では他の Ok シリーズ同様に [Apache 2.0](https://github.com/square/moshi/blob/moshi-parent-1.5.0/LICENSE.txt) でした。

----

# 導入

## 依存の追記
1行追加するだけで使えます。

```groovy:app/build.gradle
dependencies {
  compile 'com.squareup.moshi:moshi:1.5.0'
```

## Android アプリの場合は Proguard の設定
[GitHub リポジトリの README](https://github.com/square/moshi/#proguard) によると、 Proguard を使う場合は下記の設定が必要とのことです。Java アプリケーションで使う分にはこの設定は不要です。

```
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-keepclasseswithmembers class * {
    @com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
```

----

# 前提

## 実行環境

| 項目 | 値
|:---|:---
| IDE | Android Studio 2.3 & IntelliJ IDEA
| Java | 1.8.0_131
| OS | Windows 10

以下、今回の動作検証で必要なサンプルクラスの説明が続きますので、[不要であればここをクリックして飛ばしてください](#java-%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A8-json-%E3%82%92%E7%9B%B8%E4%BA%92%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B)。

## 今回使うクラス
Web ブラウザのタブを定義するクラスを扱ってみます。

```java:Tab.java
import java.util.ArrayList;
import java.util.List;

class Tab {

    private List<History> histories;

    private int index;

    private String thumbnailPath;

    private String lastTitle;

    Tab() {
        histories = new ArrayList<>();
        index = -1;
    }

    // Simple accessor methods...

    void addHistory(final History history) {
        histories.add(history);
        index++;
    }

    History getLatest() {
        return histories.get(index);
    }
}
```

Tab クラス内で使っている History クラスは下記の通り、 String 型のフィールドを2つ持つだけの簡単なクラスです。

```java:History.java
class History {

    private final String title;

    private final String url;

    private History(final String title, final String url) {
        this.title = title;
        this.url   = url;
    }

    public static History make(final String title, final String url) {
        return new History(title, url);
    }

    public String title() {
        return title;
    }

    public String url() {
        return url;
    }

}
```

## 準備
以降の手順に先立ち、Tab オブジェクトを初期化しておきます。

```java
final Tab tab = new Tab();
tab.setThumbnailPath("file://~~");
tab.setLastTitle("Google");
tab.addHistory(History.make("Title", "URL"));
```

----

# Java オブジェクトと JSON を相互変換する
Moshi では JsonAdapter というクラスで変換を実施します。

## JsonAdapter の取得
Moshi オブジェクトを生成し、そこからadapter メソッドで変換対象の型を指定して JsonAdapter のオブジェクトを取得します。

```java
final Moshi moshi = new Moshi.Builder().build();
final JsonAdapter<Tab> tabJsonAdapter = moshi.adapter(Tab.class);
```

## Java オブジェクト -> JSON の変換
JsonAdapter.toJson で実行します。

```java
final String json = tabJsonAdapter.toJson(tab);
```

### 出力
下記の JSON が出力されます。

```json
{"histories":[{"title":"Title","url":"URL"}],"index":0,"lastTitle":"Google","thumbnailPath":"file://~~"}
```

### Pretty Printing
JSON を整形した状態で出力したい場合は少し手間がかかります。

```java:pretty_print
final Buffer buffer = new Buffer(); // 1.
final JsonWriter prettyPrintWriter = JsonWriter.of(buffer); // 2.
prettyPrintWriter.setIndent("  "); // 3.
try {
    tabJsonAdapter.toJson(prettyPrintWriter, tab); // 4.
} catch (final IOException e) {
    e.printStackTrace();
}

final String json = buffer.readUtf8(); // 5
```

#### 手順

1. [Okio](https://github.com/square/okio/) の Buffer オブジェクトを用意
2. 1 の Buffer オブジェクトを対象とする JsonWriter オブジェクトを取得
3. 2 の JsonWriter に任意のインデントを設定
4. JsonAdapter.toJson に JsonWriter と Java オブジェクトを渡して実行
5. 1 の Buffer オブジェクトに書き込みが実行されているので、read して文字列を取り出す

使うクラスが増えたのと、 toJson が直接値を返さなくなったのと、toJson が例外を出すようになったので、少々戸惑うかもしれません。

#### 出力結果
下記の通り、指定したインデントで JSON が出力されました。

```json
{
  "histories": [
    {
      "title": "Title",
      "url": "URL"
    }
  ],
  "index": 0,
  "lastTitle": "Google",
  "thumbnailPath": "file://~~"
}
```

#### 備考
なお、実用性は皆無ですが、前述の手順 3. でインデントとして渡す文字列は何でも構わないようで、例えば * 2つをインデントに指定した場合は下記の JSON が出力されます。

```json
{
**"histories": [
****{
******"title": "Title",
******"url": "URL"
****}
**],
**"index": 0,
**"lastTitle": "Google",
**"thumbnailPath": "file://~~"
}
```

もちろん、この JSON を後述の JsonAdapter.fromJson に渡すと JSON としてパースできないので例外が発生します。

## JSON -> Java オブジェクトの変換
JsonAdapter.fromJson で実行します。このメソッドでは IOException が発生しうるので、そのケアが必要です。

```java
try {
    final Tab fromJson = tabJsonAdapter.fromJson(json);
} catch (IOException e) {
    e.printStackTrace();
}
```

### 出力
得られた Tab オブジェクトの中身を標準出力で表示してみます。

```java
System.out.println(fromJson.getLastTitle());
System.out.println(fromJson.getThumbnailPath());
System.out.println(fromJson.getLatest().title());
System.out.println(fromJson.getLatest().url());
```

```
Google
file://~~
Title
URL
```

独自に定義した History クラスもちゃんと変換してくれるようです。public なコンストラクタを持たないクラスであってもちゃんと変換できるのが良いと感じます。

## ソースコード
今回の動作検証用コードは gist に置いておきます。
https://gist.github.com/toastkidjp/252bc2ac86de2c0a3625b9fed0b9fc62

----

# まとめ
Moshi は軽量で扱いやすい JSON ライブラリです。ライブラリ本体に Android SDK の依存を含まないため、 Android に限らず通常の Java アプリケーション開発でも利用できます。この Moshi を使った簡単な相互変換の方法について述べました。

## 参考
- [Moshi 1.x API](http://square.github.io/moshi/1.x/moshi/)
- [json formatting with moshi (stackoverflow)](https://stackoverflow.com/questions/35026581/json-formatting-with-moshi)
- [Retrofit2.0に備えてKotlinで始めるMoshi(JSONパーサ）](http://qiita.com/droibit/items/73fe3d0ec7b11fa982ea)……Kotlin での使い方についてはこちらの記事が参考になります