Open TWiStErRob opened 5 years ago
@TWiStErRob hi, try this
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import com.google.gson.*;
class OptionalTest {
class Data {
@JsonAdapter(value = JsonNullableTypeAdapterFactory.class, nullSafe = false)
String nullProp;
String optionalProp;
}
private Gson gson;
@BeforeEach void setUp() {
gson = new GsonBuilder().create();
}
... // your tests
private static class JsonNullableTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getAdapter(type);
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
boolean serializeNulls = out.getSerializeNulls();
if (value == null && !serializeNulls) {
out.setSerializeNulls(true);
}
try {
delegate.write(out, value);
} finally {
out.setSerializeNulls(serializeNulls);
}
}
@Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
}
}
Awesome @baurchanu, thank you very much! Now I understand what nullSafe
means and its docs :) So neat and kind of simple solution in retrospect.
I flipped it around, because optionals are less frequent:
class OptionalTest {
class Data {
String nullProp;
@JsonAdapter(value = JsonOptionalTypeAdapterFactory.class, nullSafe = false)
String optionalProp;
}
@BeforeEach void setUp() {
gson = new GsonBuilder()
.serializeNulls()
.create();
}
}
/**
* Usage:
* <pre><code>
* {@code @}JsonAdapter(value = JsonOptionalTypeAdapterFactory.class, nullSafe = false)
* SomeType fieldThatShouldNotAppearInOutputWhenItIsNull;
* </code></pre>
* <p>
* Works the same way regardless of {@link GsonBuilder#serializeNulls()} setup.
* <p>
* Note: disabling {@link JsonAdapter#nullSafe()} allows us to decide what to do with null values.
*/
public class JsonOptionalTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getAdapter(type);
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
boolean originalSerializeNulls = out.getSerializeNulls();
// Only change behavior if it's null, otherwise it'll affect nested complex type's serialization.
if (value == null) {
// Make com.google.gson.stream.JsonWriter.nullValue skip the deferred name if null value.
out.setSerializeNulls(false);
}
try {
delegate.write(out, value);
} finally {
// Restore original behavior for the rest of the data.
out.setSerializeNulls(originalSerializeNulls);
}
}
@Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
}
JSON allows us to express fields that shouldn't be there and fields that could have a null value. These are different things. In Java with cannot have missing fields, so we use
null
to represent both of these states. We're interoping with an external non-Java party that has a schema like this:because
optionalProp
is not listed, it can be omitted from the JSON, but if it exists, it must have a non-null value:{ "nullProp": null, "optionalProp": "my value" }
{ "nullProp": null }
{ "nullProp": "null value", "optionalProp": "my value" }
{ "nullProp": null, "optionalProp": null }
//optionalProp
cannot be null{ }
// missing requirednullProp
we need to send valid JSON that adheres to the schema (manual tests www.jsonschemavalidator.net)
In Java we represent this with:
but the serialization behavior needs to be different for these two fields. We've looked at all the combinations of
serializeNulls
and@JsonAdapter
, but couldn't figure out how to omit the optional field when its null, while keeping the null field there if it's null.Here's a test we've been trying to pass with no luck: