Gson deserialization: how to distinguish between fields that are missing and fields that are explicitly set to null?

I am trying to implement JSON Merge Patch for the Java web service (JAX-RS) that I am creating.

The bottom line is that partial record updates are performed by sending a JSON document to the server that contains only those fields that need to be changed.

Given this entry

{ "a": "b", "c": { "d": "e", "f": "g" } } 

following JSON update document

 { "a":"z", "c": { "f": null } } 

should set a new value for "a" and remove the "f" inside the "c" .

The last problem. I do not know how I can distinguish between input where f is absent and input where f is null. Both, as far as I can tell, will be deserialized to null in the target Java object.

What to do?

+5
source share
3 answers

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.

+4
source

I think you will have to use JsonObject and look at the returned Json object.

You can use JsonParser.parse(java.io.Reader) to get JsonObject.

0
source

For primitive types, use the nullable version.

For example, use Long where you usually use long. Integer replaced by int, etc.

You can also use the String type if you want to detect missing values, but then you will also need to parse the String to the type you need.

0
source

Source: https://habr.com/ru/post/1236056/


All Articles