Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

Moshi を実際に利用する際に役立つかもしれない Tips

こんにちは。Timers で Android 版の Pairy と Famm を担当させていただいております、さわいと申します。今回は、軽量・シンプルな JSONPOJO ライブラリである Moshi についてポストさせて頂きます。実際に導入する際、こういう Tips が世の中にあればもっと気軽に導入できるかも…という内容を焦点に記述した構成となっておりますので、そもそも Moshi とはなんぞやという方は、GitHub のページを参照するか、検索してご自身で調べて下さい。

Moshi

https://github.com/square/moshi

調べたこと

  1. POJOの名前とJSONのフィールド名が異なる際の対応

  2. 謎の精度を持つタイムスタンプをどう扱うべきか

  3. 型が int か boolean かが不定なフィールドを、統一した boolean 型に変換したい時、どう扱うべきか

  4. JSON から POJO に変換する際、Moshi は JSON の型や値によって POJO にどのような値を代入するのか

  5. 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

小数系

データ型
float 32ビット単精度浮動小数点数
double 64ビット倍精度浮動小数点数

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

終わりに

JavaJSONライブラリはいくつかありましたが、Moshiは新しいだけあって、思想や設計も洗練されており、さらに提供元があのSquare様と、使う側としてはとても嬉しい限り。 Retrofit2との相性もよく、これから長い付き合いになりそうです。


子育て家族アプリFamm、カップル専用アプリPairyを運営する Timers inc. では、現在エンジニアを積極採用中!急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP : http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる