web-dev-qa-db-ja.com

将来の@NonNullフィールドを尊重するためにJSONシリアル化可能データクラスをどのように設計すればよいですか

Gson を使用してデータクラスをシリアル化/逆シリアル化し、実行間でデータを保持するアプリがあります。

私のコードは@NonNull多くのフィールド/パラメータ/メソッドが返す注釈と、1つだったもの私(または別の開発者)が新しい@NonNull注釈付きフィールド(または既存のフィールドに注釈を付けるだけでも)をデータクラスに設定すると、Gsonはそれをnullに設定します。これは、JSONが以前のバージョンのアプリで保存されたためです

今日はその質問に答えました。ユーザーガイドが言って、私のテストが確認するように:

  • この実装はnullを正しく処理します
    • シリアル化中、nullフィールドは出力からスキップされます
    • デシリアライゼーション中に、JSONにエントリがないと、オブジェクトの対応するフィールドがnullに設定されます

実際、データクラスに新しいフィールドを追加すると、nullに初期化される可能性があります。

だから私は解決策を探しました。引数なしのプライベートコンストラクターをデータクラスに追加すると、IDEに十分な情報があり、フィールドが実際にnullである可能性があることを示すことができます。例この実装の一覧を以下に示します。

public void testDataClass1() throws Exception {
    int value1 = 23;
    int value2 = 48;
    final DataClass1Version1 inputClass = new DataClass1Version1(value1, value2);
    final String dataClass1Json = getGson().toJson(inputClass);
    final DataClass1Version2 outputClass = getGson().fromJson(dataClass1Json, DataClass1Version2.class);
    assertEquals(outputClass.getValue1(), value1);
    assertEquals(outputClass.getValue2(), value2);
    assertNotNull("New @NonNull field shouldn't be null.", outputClass.getSomeNewValue());
}

DataClass1Version1.Java:

public class DataClass1Version1 {
    private int value1;
    private int value2;

    public DataClass1Version1(int value1, int value2) {
        this.value1 = value1;
        this.value2 = value2;
    }

    public int getValue1() {
        return value1;
    }

    public void setValue1(int value1) {
        this.value1 = value1;
    }

    public int getValue2() {
        return value2;
    }

    public void setValue2(int value2) {
        this.value2 = value2;
    }

    @Override
    public String toString() {
        return "DataClass1Version1{" +
                "value1=" + value1 +
                ", value2=" + value2 +
                '}';
    }
}

DataClass1Version2.Java:

import Android.support.annotation.NonNull;
import Android.support.annotation.Nullable;

public class DataClass1Version2 {
    private int value1;
    private int value2;
    @Nullable
    private String someNullableString;
    //@NonNull
    //private String someNewValue; //you'll get a warning for not initializing this value
    @NonNull
    private String someNewValue = ""; //if you don't initialize this field here, it could be null after json deserialization

    private DataClass1Version2() { 
        //adding this constructor caused a warning when someNewValue didn't have an initializer
    }

    public DataClass1Version2(int value1, int value2, @Nullable String someNullableString, @NonNull String someNewValue) {
        this.value1 = value1;
        this.value2 = value2;
        this.someNullableString = someNullableString;
        this.someNewValue = someNewValue;
    }

    public int getValue1() {
        return value1;
    }

    public void setValue1(int value1) {
        this.value1 = value1;
    }

    public int getValue2() {
        return value2;
    }

    public void setValue2(int value2) {
        this.value2 = value2;
    }

    @Nullable
    public String getSomeNullableString() {
        return someNullableString;
    }

    public void setSomeNullableString(@Nullable String someNullableString) {
        this.someNullableString = someNullableString;
    }

    @NonNull
    public String getSomeNewValue() {
        return someNewValue;
    }

    public void setSomeNewValue(@NonNull String someNewValue) {
        this.someNewValue = someNewValue;
    }

    @Override
    public String toString() {
        return "DataClass1Version2{" +
                "value1=" + value1 +
                ", value2=" + value2 +
                ", someNullableString='" + someNullableString + '\'' +
                ", someNewValue='" + someNewValue + '\'' +
                '}';
    }
}

このソリューションの問題は、通常、データクラスでfinalフィールドを使用するため、引数のないコンストラクターが機能しないことです。私は何をすべきか?

これを書いているときに、いくつかのデフォルト値に初期化することを考えましたが、Gsonはリフレクションを使用しているため、最終的なフィールドであっても、それらのデフォルトを上書きします。私はまだ他の人がどう思うか知りたいです。これにより、逆シリアル化に追加の重みが追加されますか? ArrayListの束があり、引数なしのコンストラクターで新しいインスタンスを作成した場合、新しいインスタンスはGsonが生成したインスタンスで即座に置き換えられませんか?

以下は、最終フィールドとデフォルトの初期化子を含むDataClass2Version2です。

import Android.support.annotation.NonNull;
import Android.support.annotation.Nullable;

public class DataClass2Version2 {
    private final int value1;
    private final int value2;
    @Nullable
    private final String someNullableString;
    @NonNull
    private final String someNewValue;

    private DataClass2Version2() {
        value1 = 0;
        value2 = 0;
        someNullableString = null;
        someNewValue = "";
    }

    public DataClass2Version2(int value1, int value2, @Nullable String someNullableString, @NonNull String someNewValue) {
        this.value1 = value1;
        this.value2 = value2;
        this.someNullableString = someNullableString;
        this.someNewValue = someNewValue;
    }

    public int getValue1() {
        return value1;
    }

    public int getValue2() {
        return value2;
    }

    @Nullable
    public String getSomeNullableString() {
        return someNullableString;
    }

    @NonNull
    public String getSomeNewValue() {
        return someNewValue;
    }

    @Override
    public String toString() {
        return "DataClass2Version2{" +
                "value1=" + value1 +
                ", value2=" + value2 +
                ", someNullableString='" + someNullableString + '\'' +
                ", someNewValue='" + someNewValue + '\'' +
                '}';
    }
}
6
Jon

フィールドを含まない(またはフィールドがnullである)野生のファイルがあり、解析する必要がある場合、定義によりフィールドはnull可能です。 @NotNullはそのような場合には不適切です。

それで、あなたは何ができますか?

まず、次のように、ファイルのトップレベルにバージョン番号を付けることができます。

{
 "version": 1,
 ...other fields...
}

次に、バージョン1のファイルを解析するためのクラスが1つ、バージョン2のファイル用に1つのクラスが続きます。 null可能フィールドを古いクラスにスローし、null不可フィールドを新しいクラスにスローします。次に、共通のインターフェースと(おそらく)古いクラスを最新のクラスに変換するメソッドが必要です。

これには重要な欠点があります。これを頻繁に行うと、多くのクラスが作成されることになります。残念ながら、私が提供できる最善のアドバイスは「これをあまり行わないこと」です。実際のところ、ライブ(実稼働)データを取得すると、スキーマの変更は困難になります。 SQL、JSON、またはまったく異なるものを使用しているかどうかは関係ありません。レガシーデータを何らかの方法で処理する準備をする必要があります。どのように処理するかに関係なく、これは常に困難な作業になります。

2
Kevin

あなたは技術的解決策を見ていますが、実際にはデータを見なければなりません。

文字列値を持つフィールドABCを予期している場合、JSONデータには次のものを含めることができます:なし(フィールドはありません)、null(JSONにはnullが含まれます)、空の文字列(JSONには空の文字列が含まれます)、古いごみ(おそらく数値、おそらく配列)、または空でない文字列。

そのデータの解釈方法を決定する必要があります。 ABCで受け取る内容に応じて、これによりレコード全体が無効になると判断できます(たとえば、文字列を予期した配列がある場合)、数値を文字列に変換したり、値を受け入れたり、nullとして受け入れたりする場合があります。空の文字列の場合、値を受け入れないか、nullオブジェクトとしてnullを受け入れる可能性があります。

それはあなたがする必要がある本当の決定です。もちろん、ABCを@nonnullとして宣言すると、ABCをnull値にするような解釈は不可能になります。 JSONデータに何も含まれていないかnull値がある場合、これを空の文字列または無効なレコードとして解釈することのみを決定できます。これは、null値として解釈することができなくなったためです。

1
gnasher729