こんにちは。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