こんにちは。Timers で Android 版の Pairy と Famm を担当させていただいております、さわいと申します。今回は、軽量・シンプルな JSON ⇔ POJO ライブラリである Moshi についてポストさせて頂きます。実際に導入する際、こういう Tips が世の中にあればもっと気軽に導入できるかも…という内容を焦点に記述した構成となっておりますので、そもそも Moshi とはなんぞやという方は、GitHub のページを参照するか、検索してご自身で調べて下さい。
Moshi
https://github.com/square/moshi
調べたこと
謎の精度を持つタイムスタンプをどう扱うべきか
型が int か boolean かが不定なフィールドを、統一した boolean 型に変換したい時、どう扱うべきか
JSON から POJO に変換する際、Moshi は JSON の型や値によって POJO にどのような値を代入するのか
JSON から POJO に変換する際、POJO にフィールドが定義されていても、JSON にはフィールド・値が存在しない場合、Moshi は POJO にどのようなデフォルト値を代入するのか
調べなかったこと
Moshi の Wiki に書いてあること
サンプルコードを見ればわかること
Tips
1. POJOの名前とJSONのフィールド名が異なる際の対応
アノテーションがあるので、そこで名前を定義すればOK。
public class Hello { @Nullable @Json(name = "1") private final String one; @NonNull @Json(name = "nick_name") private final String nickName; }
2. 謎の精度を持つタイムスタンプをどう扱うべきか
PHPで
<?php microtime(true);
でタイムスタンプを取ってそのまま返却するようなAPIだと、例えば1000000003.0002といった、なんとも言えない精度で返される。これは困るので、その対応。
NazoTimestamp.java
@Retention(RetentionPolicy.RUNTIME) @JsonQualifier public @interface NazoTimestamp { }
NazoTimestampAdapter.java
public class NazoTimestampAdapter { @ToJson String toJson(@NazoTimestamp BigDecimal bigDecimal) { return bigDecimal.toPlainString(); } @FromJson @NazoTimestamp BigDecimal fromJson(String json) { return new BigDecimal(json); } }
あとはMoshiのBuilderにAdapterをかまして、POJO側では
public class Ore { @NonNull @NazoTimestamp private final BigDecimal timestamp; }
とすればOK。
3. 型が int か boolean かが不定なフィールドを、統一した boolean 型に変換したい時、どう扱うべきか
基本的に2.と同様。 カスタムAdapterを作成する。
public class NazoBoolAdapter { @ToJson String toJson(@NazoBool boolean bool) { return String.valueOf(bool); } @FromJson @NazoBool boolean fromJson(JsonReader reader) throws JsonDataException { try { if (reader.peek().equals(JsonReader.Token.NUMBER)) { return reader.nextInt() == 1; } else if (reader.peek().equals(JsonReader.Token.BOOLEAN)) { return reader.nextBoolean(); } } catch (IOException e) { e.printStackTrace(); } return false; } }
@Retention(RetentionPolicy.RUNTIME) @JsonQualifier public @interface NazoBool { }
あとは Moshi の Builder に Adapter をかまして、POJO 側では
public class Ore { @NazoBool private final boolean nazoBool; }
のように使用すればOK。
4. JSON から POJO に変換する際、Moshi は JSON の値によって POJO にどのような値を代入するのか
JSONの値が、POJOで指定された型と異なる場合に関しても、Moshiがどのように変換してくれるかを検証。
整数系
- 検証コード
public class Card { public [short | int | long] number; }
{"number": <代入値>}
- 検証結果
| <代入値> | 変換可/否 | POJO の number に入る値 |
|---|---|---|
| 1234 | OK | 1234 |
| null | NG | 変換エラーでクラッシュ |
| "1234" | OK | 1234 |
| false | NG | 変換エラーでクラッシュ |
| {"key", "value"} | NG | 変換エラーでクラッシュ |
小数系
- 検証コード
public class Card { public [float | double] number; }
"{"number": <代入値>}"
- 検証結果
| <代入値> | 変換可/否 | POJO の number に入る値 |
|---|---|---|
| 1234.11 | OK | 1234.11 |
| null | NG | 変換エラーでクラッシュ |
| "1234.12" | OK | 1234.12 |
| false | NG | 変換エラーでクラッシュ |
| {"key", "value"} | NG | 変換エラーでクラッシュ |
[補足] JSON における数字型問題
Moshi では、int, long, double はNUMBERという概念で認識される模様(float は不明だが、double扱い?)。
なので、
{"value": 1.2234322}
のように書いても、POJOで定義された型が適切であれば適切にパースしてくれる模様(この場合は float か double を指定するのが正解)。
[補足]:Javaの数字まわりのデータ型
整数系
| データ型 | 値 |
|---|---|
| short | 16ビット整数 -32768~32767 |
| int | 32ビット整数 -2147483648~2147483647 |
| long | 64ビット整数 -9223372036854775808~9223372036854775807 |
小数系
| データ型 | 値 |
|---|---|
| float | 32ビット単精度浮動小数点数 |
| double | 64ビット倍精度浮動小数点数 |
※Android Java (JDK 7) には unsigned はありません。JDK 8 からです。
String
- 検証コード
public class Card { public String name; }
{"name": <代入値>}
- 検証結果
| <代入値> | 変換可/否 | POJO の name に入る値 |
|---|---|---|
| null | OK | null(メソッドを呼び出すとクラッシュする) |
| "" | OK | 空文字列(メソッドを呼び出してもクラッシュしない) |
| 0 | OK | "0" |
| 1.234 | OK | "1.234" |
| false | NG | 変換エラーでクラッシュ |
boolean
- 検証コード
public class Card { public boolean isPremium; }
{"isPremium": <代入値>}
- 検証結果
| <代入値> | 変換可/否 | POJO の isPremium に入る値 |
|---|---|---|
| true | OK | true |
| false | OK | false |
| null | NG | 変換エラーでクラッシュ |
| "true" | NG | 変換エラーでクラッシュ |
| 0 | NG | 変換エラーでクラッシュ |
| 1.234 | NG | 変換エラーでクラッシュ |
| [] | NG | 変換エラーでクラッシュ |
| {} | NG | 変換エラーでクラッシュ |
array, list
- 検証コード
public class Card { public [String[] | List<String>] weakTypes; }
{"weakTypes": <代入値>}
- 検証結果
| <代入値> | 型 | 変換可/否 | POJO の weakTypes に入る値 |
|---|---|---|---|
| null | List | OK | null(メソッドを呼び出すとクラッシュする) |
| null | Array | OK | null(メソッドを呼び出すとクラッシュする) |
| [] | List | OK | データを持たないListオブジェクト(メソッドを呼び出してもクラッシュしない) |
| [] | Array | OK | データを持たないArrayオブジェクト(メソッドを呼び出してもクラッシュしない) |
| ["water", "fire"] | List | OK | get(0): water, get(1): fire |
| ["water", "fire"] | Array | OK | [0]: water, [1]: fire |
| {"water", "fire"} | List | NG | 変換エラーでクラッシュ |
| {"water", "fire"} | Array | NG | 変換エラーでクラッシュ |
Object in Object
- 検証コード
public class Card { public Frame frame; public static class Frame { String color; int size; } }
{"frame": <代入値>}
- 検証結果
| <代入値> | 変換可/否 | POJO の frame に入る値 |
|---|---|---|
| {} | NG | 変換エラーでクラッシュ |
| null | OK | null |
| {"color": "red", "size": 20} | OK | frame.color = "red", frame.size = 20 |
5. JSON から POJO に変換する際、POJO にフィールドが定義されていても、JSON にはフィールド・値が存在しない場合、Moshi はどのようなデフォルト値を代入するのか
こんなデフォルト値が入る。
| データ型 | 値 |
|---|---|
| String型(String[]も含む) | null |
| List型 | null |
| オブジェクト型({}←これで囲まれたもの) | null |
| boolean型 | false |
| short, int, long の整数系 | 0 |
| float, double の小数系 | 0.0 |
終わりに
JavaのJSONライブラリはいくつかありましたが、Moshiは新しいだけあって、思想や設計も洗練されており、さらに提供元があのSquare様と、使う側としてはとても嬉しい限り。 Retrofit2との相性もよく、これから長い付き合いになりそうです。
子育て家族アプリFamm、カップル専用アプリPairyを運営する Timers inc. では、現在エンジニアを積極採用中!急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP : http://timers-inc.com/engineerings