I confirm mlk's answer, but given that I already have (and nevertheless need) a POJO representation of a JSON object, I believe that rendering automatically is still better than manually searching.
The problem is that, as I said, both absent and explicit null values ββare set to zero in the corresponding POJO , which will be populated by gson.fromJson(...) . (Unlike, for example, R NULL and NA , Java has only one representation for "does not exist.")
However, by modeling my data structure using Java 8 Optionals, I can do just that: distinguish between something that is not set and something that is set to NULL . Here is what I ended up with:
1) I replaced all the fields in my Optional<T> data objects.
public class BasicObjectOptional { private Optional<String> someKey; private Optional<Integer> someNumber; private Optional<String> mayBeNull; public BasicObjectOptional() { } public BasicObjectOptional(boolean initialize) { if (initialize) { someKey = Optional.ofNullable("someValue"); someNumber = Optional.ofNullable(42); mayBeNull = Optional.ofNullable(null); } } @Override public String toString() { return String.format("someKey = %s, someNumber = %s, mayBeNull = %s", someKey, someNumber, mayBeNull); } }
Or nested:
public class ComplexObjectOptional { Optional<String> theTitle; Optional<List<Optional<String>>> stringArray; Optional<BasicObjectOptional> theObject; public ComplexObjectOptional() { } public ComplexObjectOptional(boolean initialize) { if (initialize) { theTitle = Optional.ofNullable("Complex Object"); stringArray = Optional.ofNullable(Arrays.asList(Optional.ofNullable("Hello"),Optional.ofNullable("World"))); theObject = Optional.ofNullable(new BasicObjectOptional(true)); } } @Override public String toString() { return String.format("theTitle = %s, stringArray = %s, theObject = (%s)", theTitle, stringArray, theObject); } }
2) A serializer and deserializer have been introduced based on this useful SO answer .
public class OptionalTypeAdapter<E> extends TypeAdapter<Optional<E>> { public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { //@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { Class<T> rawType = (Class<T>) type.getRawType(); if (rawType != Optional.class) { return null; } final ParameterizedType parameterizedType = (ParameterizedType) type.getType(); final Type actualType = parameterizedType.getActualTypeArguments()[0]; final TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(actualType)); return new OptionalTypeAdapter(adapter); } }; private final TypeAdapter<E> adapter; public OptionalTypeAdapter(TypeAdapter<E> adapter) { this.adapter = adapter; } @Override public void write(JsonWriter out, Optional<E> value) throws IOException { if(value == null || !value.isPresent()){ out.nullValue(); } else { adapter.write(out, value.get()); } } @Override public Optional<E> read(JsonReader in) throws IOException { final JsonToken peek = in.peek(); if(peek != JsonToken.NULL){ return Optional.ofNullable(adapter.read(in)); } in.nextNull(); return Optional.empty(); } }
3) Registered this adapter when initializing Gson.
Gson gsonOptFact = new GsonBuilder() .serializeNulls() // matter of taste, just for output anyway .registerTypeAdapterFactory(OptionalTypeAdapter.FACTORY) .create();
This allows me to write JSON in such a way that both NULL and the empty Optional serialized as NULL (or simply removed from the output) and at the same time read the JSON into the Optional fields, so if the field is NULL I know that it was not in the JSON input , and if the field is Optional.empty , I know that it was set to NULL in the input.
Example:
System.out.println(gsonOptFact.toJson(new BasicObjectOptional(true))); // {"someKey":"someValue","someNumber":42,"mayBeNull":null} System.out.println(gsonOptFact.toJson(new ComplexObjectOptional(true))); // {"theTitle":"Complex Object","stringArray":["Hello","World"],"theObject":{"someKey":"someValue","someNumber":42,"mayBeNull":null}} // Now read back in: String basic = "{\"someKey\":\"someValue\",\"someNumber\":42,\"mayBeNull\":null}"; String complex = "{\"theTitle\":\"Complex Object\",\"stringArray\":[\"Hello\",\"world\"],\"theObject\":{\"someKey\":\"someValue\",\"someNumber\":42,\"mayBeNull\":null}}"; String complexMissing = "{\"theTitle\":\"Complex Object\",\"theObject\":{\"someKey\":\"someValue\",\"mayBeNull\":null}}"; BasicObjectOptional boo = gsonOptFact.fromJson(basic, BasicObjectOptional.class); System.out.println(boo); // someKey = Optional[someValue], someNumber = Optional[42], mayBeNull = Optional.empty ComplexObjectOptional coo = gsonOptFact.fromJson(complex, ComplexObjectOptional.class); System.out.println(coo); // theTitle = Optional[Complex Object], stringArray = Optional[[Optional[Hello], Optional[world]]], theObject = (Optional[someKey = Optional[someValue], someNumber = Optional[42], mayBeNull = Optional.empty]) ComplexObjectOptional coom = gsonOptFact.fromJson(complexMissing, ComplexObjectOptional.class); System.out.println(coom); // theTitle = Optional[Complex Object], stringArray = null, theObject = (Optional[someKey = Optional[someValue], someNumber = null, mayBeNull = Optional.empty])
I think this will allow me to integrate the JSON Merge patch well with my existing data objects.