I have an app that uses Gson to serialize/deserialize data classes and persist data between runs.
My code uses @NonNull
annotations for many fields/parameters/method returns and one thing that was What if I (or another developer) adds a new @NonNull
annotated field (or even just annotates an existing field) to a data class and Gson sets it to null
because the JSON was saved in a previous version of the app?
Well I answered that question today. As the user guide says and my tests confirm:
- This implementation handles nulls correctly
- While serialization, a null field is skipped from the output
- While deserialization, a missing entry in JSON results in setting the corresponding field in the object to null
Indeed, if I add a new field to my data class, it might be initialized to null
.
So I looked for a solution. I found that if I add a private no-args constructor to the data class, there is enough information for the IDE to flag that the field could indeed be null
. An example of this implementation is listed below.
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 + ''' +
'}';
}
}
The problem with this solution is that I generally use final fields in my data classes and as such, a no-args constructor won’t work. What should I do?
While writing this up, I thought of just initializing to some default values and then I guess since Gson uses reflection, it will overwrite those defaults, even though they are final fields. I still want to know what others think. Does this add extra weight to deserialization; if I have a bunch of ArrayLists and I create new instances in the no-args constructor, won’t the new instances be instantly replaced with the instance Gson generated?
Here’s DataClass2Version2 with final fields and default initializers:
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 + ''' +
'}';
}
}