こんにちは。Timers で Android 版の Pairy と Famm を担当させていただいております、さわいと申します。今回は、軽量・シンプルな JSON ⇔ POJO ライブラリである Moshi についてポストさせて頂きます。実際に導入する際、こういう Tips が世の中にあればもっと気軽に導入できるかも…という内容を焦点に記述した構成となっておりますので、そもそも Moshi とはなんぞやという方は、GitHub のページを参照するか、検索してご自身で調べて下さい。
Moshi
https://github.com/square/moshi
調べたこと
POJOの名前とJSONのフィールド名が異なる際の対応
謎の精度を持つタイムスタンプをどう扱うべきか
型が 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がどのように変換してくれるかを検証。
整数系
POJO
public class Card {
public [short | int | long] number;
}
JSON
{"number": <代入値>}
<代入値> |
変換可/否 |
POJO の number に入る値 |
1234 |
OK |
1234 |
null |
NG |
変換エラーでクラッシュ |
"1234" |
OK |
1234 |
false |
NG |
変換エラーでクラッシュ |
{"key", "value"} |
NG |
変換エラーでクラッシュ |
小数系
POJO
public class Card {
public [float | double] number;
}
JSON
"{"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扱い?)。
https://github.com/square/moshi/blob/master/moshi/src/main/java/com/squareup/moshi/JsonReader.java#L1418
なので、
{"value": 1.2234322}
のように書いても、POJOで定義された型が適切であれば適切にパースしてくれる模様(この場合は float か double を指定するのが正解)。
[補足]:Javaの数字まわりのデータ型
整数系
データ型 |
値 |
short |
16ビット整数 -32768~32767 |
int |
32ビット整数 -2147483648~2147483647 |
long |
64ビット整数 -9223372036854775808~9223372036854775807 |
小数系
※Android Java (JDK 7) には unsigned はありません。JDK 8 からです。
String
POJO
public class Card {
public String name;
}
JSON
{"name": <代入値>}
<代入値> |
変換可/否 |
POJO の name に入る値 |
null |
OK |
null(メソッドを呼び出すとクラッシュする) |
"" |
OK |
空文字列(メソッドを呼び出してもクラッシュしない) |
0 |
OK |
"0" |
1.234 |
OK |
"1.234" |
false |
NG |
変換エラーでクラッシュ |
boolean
POJO
public class Card {
public boolean isPremium;
}
JSON
{"isPremium": <代入値>}
<代入値> |
変換可/否 |
POJO の isPremium に入る値 |
true |
OK |
true |
false |
OK |
false |
null |
NG |
変換エラーでクラッシュ |
"true" |
NG |
変換エラーでクラッシュ |
0 |
NG |
変換エラーでクラッシュ |
1.234 |
NG |
変換エラーでクラッシュ |
[] |
NG |
変換エラーでクラッシュ |
{} |
NG |
変換エラーでクラッシュ |
array, list
POJO
public class Card {
public [String[] | List<String>] weakTypes;
}
JSON
{"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
POJO
public class Card {
public Frame frame;
public static class Frame {
String color;
int size;
}
}
JSON
{"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