How to use java.util.Optional with REST API? - java

I have a class that looks like
public class ActiveDirectorySetup implements Serializable {
private ActiveDirectoryDataSource activeDirectoryDataSource;
private Optional<ShActiveDirectorySettings> shActiveDirectorySettings;
private Optional<SaActiveDirectorySettings> saActiveDirectorySettings;
// ...
}
I send this over the API as
Optional<ActiveDirectoryConfiguration> configuration = store.getConfiguration();
if (configuration.isPresent()) {
return configuration.get();
}
What I see on the browser is
[
{
"activeDirectoryDataSource":{
"host":"localhost",
"port":0,
"userName":"user",
"password":"password",
"activeDirectoryQueryConfig":{
"base":{
"present":false
},
"filter":"filter",
"attributes":[
]
},
"activeDirectorySslSettings":{
"present":false
}
},
"shActiveDirectorySettings":{
"present":true
},
"saActiveDirectorySettings":{
"present":true
}
}
]
for a payload that looks like
{
"activeDirectorySetups": [
{
"activeDirectoryDataSource": {
"host": "localhost",
"port": 0,
"userName": "user",
"password": "password",
"activeDirectoryQueryConfig": {
"base": null,
"filter": "filter",
"attributes": []
},
"activeDirectorySslSettings": null
},
"shActiveDirectorySettings": {
"enableUserMapping": true,
"attributes": null
},
"saActiveDirectorySettings": null
}
]
}
As you could see, I get {"present":true} instead of the actual value.
I am using jackson-datatype-jdk8 for this work. How can I force it to replace {"present":true} with actual values - either null or
{"enableUserMapping": true, "attributes": null}

I'm pretty sure you'd need to write custom serialization / deserialization functionality for this.
Deserializer
public class OptionalDeserializer<T> extends StdDeserializer<Optional<T>> {
private ObjectMapper customObjectMapper;
private Class<T> type;
/**
* #param customObjectMapper any ObjectMapper, possibly with deserialization logic for the wrapped type
* #param type the wrapped type
*/
public OptionalDeserializer(ObjectMapper customObjectMapper, Class<T> type) {
this(Optional.class);
this.customObjectMapper = customObjectMapper;
this.type = type;
}
// At least one type-based constructor is required by Jackson
private OptionalDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Optional<T> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
// Read entire tree
JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser);
// Check if "present" is true
if (treeNode.has("present") && treeNode.get("present").asBoolean()) {
// Read your wrapped value
return Optional.of(customObjectMapper.treeToValue(treeNode.get("data"), type));
}
// Return empty() by default
return Optional.empty();
}
}
Serializer
Note you could include a custom ObjectMapper for the Box type anywhere in the pipeline. It's omitted in the serializer for simplicity.
public class OptionalSerializer<T> extends StdSerializer<Optional<T>> {
public OptionalSerializer(Class<T> type) {
this(TypeFactory.defaultInstance().constructParametricType(Optional.class, type));
}
protected OptionalSerializer(JavaType javaType) {
super(javaType);
}
#Override
public void serialize(Optional<T> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
if (value.isPresent()) {
gen.writeBooleanField("present", true);
gen.writeObjectField("data", value.get());
} else {
gen.writeBooleanField("present", false);
}
gen.writeEndObject();
}
}
Example usage:
public static void main(String[] args) throws IOException {
ObjectMapper optionalMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
// Add any custom deserialization logic for Box objects to this mapper
ObjectMapper boxMapper = new ObjectMapper();
OptionalDeserializer<Box> boxOptionalDeserializer = new OptionalDeserializer<>(boxMapper, Box.class);
OptionalSerializer<Box> boxOptionalSerializer = new OptionalSerializer<>(Box.class);
module.addDeserializer(Optional.class, boxOptionalDeserializer);
// use addSerializer(JsonSerializer<?> ser), not addSerializer(Class<? extends T> type, JsonSerializer<T> ser)
// The generic types involved here will otherwise not let the program compile
module.addSerializer(boxOptionalSerializer);
optionalMapper.registerModule(module);
String json = "{\"present\": true, \"data\": {\"myValue\": 123}}";
Optional optional = optionalMapper.readValue(json, Optional.class);
#SuppressWarnings("unchecked") // Guaranteed safe cast
Optional<Box> boxOptional = (Optional<Box>) optional;
// Prints "123"
boxOptional.ifPresent(box -> System.out.println(box.getMyValue()));
// Prints the contents of "json" (variable defined above)
System.out.println(optionalMapper.writeValueAsString(boxOptional));
}
Where Box is just a simple example class:
private static class Box {
private int myValue;
public int getMyValue() {
return myValue;
}
public void setMyValue(int myValue) {
this.myValue = myValue;
}
}

I think you are relying on default java serialization while using Optional in Java 8.
Kindly note that Optional is not serializable and hence, you will have to write your own JSON serializer/deserializer.

Related

Jackson: Deserializing a polymorphic list

I would like to implement a custom deserializer for our REST API that is not only used by Java application. Therefore I don't want to have Jackson putting type information into the serialized JSON.
I'm currently struggling with deserializing CollectionExpand since it contains a list data of specific ResourceModel.
public class EntityModel<R extends ResourceModel> implements Serializable {
private R data;
private List<ResourceLink> links;
private List<CollectionExpand> expands;
}
public class CollectionExpand {
private String name;
// Resource Model is an interface
private Collection<ResourceModel> data;
}
ResourceModel is an interface an each CollectionExpand contains a collection of one type of ResourceModel per name.
For example a json output could look like this.
{
"data": {},
"links": [],
"expand": [
{
"name": "photos",
"data": [
{
"id": 12,
"name": "hello.jpg"
},
{
"id": 12,
"name": "hello.jpg"
}
]
},
{
"name": "persons",
"data": [
{
"id": 783378,
"name": "Peter",
"age": 12
},
{
"id": 273872,
"name": "Maria",
"age": 77
}
]
}
]
}
As you can see each name contains the same type of resource model. photos contains PhotoResourceModel and person contains PersonResourceModel.
I started to implement my custom Jackson Deserializer
public class CollectionExpandDeserializer extends StdDeserializer<CollectionExpand> {
public CollectionExpandDeserializer() {
super(CollectionExpand.class);
}
#Override
public CollectionExpand deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
CollectionExpand collectionExpand = new CollectionExpand();
if (Objects.equals(p.nextFieldName(), "name")) {
collectionExpand.setName(p.nextTextValue());
}
if (Objects.equals(p.nextFieldName(), "data")) {
// depending on the field name I would like to delegate the deserialization to a specific type.
if (name.equals("photos") {
// how to do this?
collectionExpand.setData(/* deserialize to a list of PhotoResource */);
}
}
return collectionExpand;
}
I'm current stuck on how can I delegate telling Jackson to deserialize this as a PhotoResource list.
In general is this the right approach or is there another way to do it (without putting any Jackson meta data into the JSON while serialization)?
I have ended up implementing my custom deserializer as below
#Override
public CollectionExpand deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
JsonNode node = ctx.readTree(p);
CollectionExpand collectionExpand = new CollectionExpand();
collectionExpand.setName(node.get("name").asText());
ArrayNode data = node.withArray("data");
Iterator<JsonNode> iterator = data.iterator();
while (iterator.hasNext()) {
Class<? extends ResourceModel> aClass = resolveClass(collectionExpand.getName());
if (aClass != null) {
JsonNode jsonNode = iterator.next();
collectionExpand.getData().add(p.getCodec().treeToValue(jsonNode, aClass));
}
}
return collectionExpand;
}
private Class<? extends ResourceModel> resolveClass(String name) {
if ("contents".equals(name)) {
return ContentsGetResourceModel.class;
} else if ("tags".equals(name)) {
return TagsGetResourceModel.class;
} else {
return null;
}
}
I took my a while to understand how to deserialize a JsonNode/TreeNode into a specific type. In the end I learned that this can be basically done by using the parsers codec.
PhotoResource photoResource = p.getCodec().treeToValue(jsonNode, PhotoResource.class);

Serializing and deserializing JSON targeting JAVA classes with Jackson

Trying to deserialize/serialize JSON into Java beans I've created. Really new to Jackson and this endeavor, so bear with me. I have the following:
{
"foo": {
"firstBlock": {
"myValue": 1,
"someBool": true,
"stringValue": "OK"
},
"anotherBlock": {
"values": [
{
"yikes01": 42
},
{
"yikes02": 215
}
],
"myInt": 64,
"logging": "Yes"
}
}
}
My Java beans are broken down into several as the objects in the JSON are used repeatedly, so it would be:
#JsonRootName("foo")
public class FooBean {
private FirstBlockBean firstBlock;
private AnotherBlockBean anotherBlock;
#JsonGetter("firstBlock")
public FirstBlockBean getFirstBlock() { return firstBlock; }
#JsonSetter("firstBlock")
public void setFirstBlock(FirstBlockBean firstBlock) { this.firstBlock = firstBlock; }
#JsonGetter("anotherBlock")
public AnotherBlockBean getAnotherBlock() { return anotherBlock; }
#JsonSetter("firstBlock")
public void setAnotherBlock(AnotherBlockBean anotherBlock) { this.anotherBlock = anotherBlock; }
}
#JsonRootName("firstBlock")
public class FirstBlockBean {
private int myValue;
private Boolean someBool;
private String stringValue;
#JsonGetter("myValue")
public int getMyValue() { return myValue; }
#JsonSetter("myValue")
public void setMyValue(int myValue) { this.myValue = myValue; }
#JsonGetter("someBool")
public Boolean getSomeBool() { return someBool; }
#JsonSetter("someBool")
public void setSomeBool(Boolean someBool) { this.someBool = someBool; }
#JsonGetter("stringValue")
public String getStringValue() { return stringValue; }
#JsonSetter("someBool")
public void setStringValue(String stringValue) { this.stringValue = stringValue; }
}
...and AnotherBlockBean class implemented in similar fashion (omitted for brevity.) I'm using Jackson for this, and my question is - is there a mechanism in Jackson for serializing and deserializing for this case? Ideally I'd like something along the lines of (pseudo-code below because I've not been able to surface anything via Google searches or searches on here):
// Assume "node" contains a JsonNode for the tree and foo is an uninitialized FooBean class object.
JsonHelper.deserialize(node, FooBean.class, foo);
At this point I'd be able to read the values back:
int i = foo.getFirstBlock().getMyValue();
System.out.println("i = " + i); // i = 1
Similarly I'd like to be able to take the foo instance and serialize it back into JSON with another method. Am I dreaming for wanting this sort of built-in functionality or does it exist?
The main class when working with Jackson is the ObjectMapper. It has a lot of options, take a look at the available methods.
This is an example of a typical helper class that uses the ObjectMapper to convert between Java objects and Strings.
public class JsonHelper {
private ObjectMapper objectMapper;
public JsonHelper(){
this.objectMapper = new ObjectMapper();
// Your mapping preferences here
this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.objectMapper.setSerializationInclusion(Include.NON_NULL);
this.objectMapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
}
public String serialize(Object object) {
try {
return this.objectMapper.writeValueAsString(object);
} catch (Exception e) {
// TODO Handle exception
return null;
}
}
public <T> T deserialize(String json, Class<T> clazz) {
try {
return this.objectMapper.readValue(json, clazz);
} catch (Exception e) {
// TODO Handle exception
return null;
}
}
public <T> T deserialize(String json, TypeReference<T> valueTypeRef) {
try {
return this.objectMapper.readValue(json, valueTypeRef);
} catch (Exception e) {
// TODO Handle exception
return null;
}
}
}
Some tips:
If the name of the getter and setter methods follows the usual convention, you can omit the #JsonGetter and #JsonSetter annotations and just use the #JsonProperty annotation in the field declaration
If the name of the java field is equal to the node name in the JSON, you can also omit the #JsonProperty annotation (Jackson will map JSON nodes and Java fields with matching names).

Using jackson deserialising a property which can be List of object or the object

My lib is calling an API which can return either of the following JSON structure -
{
"key_is_same" : {
"inner" : "val"
}
}
-or-
{
"key_is_same" : [{
"inner" : "val1"
},
{
"inner" : "val2"
}
]
}
Is there any annotation in jakson which can handle this and deserializ it into respective type
Looks like you are looking for the ACCEPT_SINGLE_VALUE_AS_ARRAY deserialization feature.
Feature that determines whether it is acceptable to coerce non-array (in JSON) values to work with Java collection (arrays, java.util.Collection) types. If enabled, collection deserializers will try to handle non-array values as if they had "implicit" surrounding JSON array. This feature is meant to be used for compatibility/interoperability reasons, to work with packages (such as XML-to-JSON converters) that leave out JSON array in cases where there is just a single element in array.
Feature is disabled by default.
It could be enabled either in ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
Or via the #JsonFormat annotation:
#JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Foo> oneOrMany;
For illustration purposes, consider the following JSON documents:
{
"oneOrMany": [
{
"value": "one"
},
{
"value": "two"
}
]
}
{
"oneOrMany": {
"value": "one"
}
}
It could be the deserialized to the following classes:
#Data
public class Foo {
private List<Bar> oneOrMany;
}
#Data
public class Bar {
private String value;
}
Just ensure the feature is enabled in your ObjectMapper or your field is annotated with #JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY).
And in case you are looking for the equivalent feature for serialization, refer to WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED.
I would recommend using Object as your data type for the property which is dynamic. So Here is my sample.
import java.util.Arrays;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class MainObject {
private Object key_is_same;
public Object getKey_is_same() {
return key_is_same;
}
public void setKey_is_same(Object key) {
this.key_is_same = key;
}
public static class KeyObject {
private String inner;
public String getInner() {
return inner;
}
public void setInner(String inner) {
this.inner = inner;
}
}
public static void main(String...s) throws JsonProcessingException {
MainObject main = new MainObject();
KeyObject k = new KeyObject();
k.setInner("val1");
main.setKey_is_same(k);
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(main));
main.setKey_is_same(Arrays.asList(k, k));
System.out.println(om.writeValueAsString(main));
public static void main(String...s) throws IOException {
MainObject main = new MainObject();
KeyObject k = new KeyObject();
k.setInner("val1");
main.setKey_is_same(k);
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(main));
main.setKey_is_same(Arrays.asList(k, k));
System.out.println(om.writeValueAsString(main));
// Deserialize
MainObject mainWithObject = om.readValue("{\"key_is_same\":{\"inner\":\"val1\"}}", MainObject.class);
MainObject mainWithList = om.readValue("{\"key_is_same\":[{\"inner\":\"val1\"},{\"inner\":\"val1\"}]}", MainObject.class);
if(mainWithList.getKey_is_same() instanceof java.util.List) {
((java.util.List) mainWithList.getKey_is_same()).forEach(System.out::println);
}
}
}
}
Output
{"key_is_same":{"inner":"val1"}}
{"key_is_same":[{"inner":"val1"},{"inner":"val1"}]}

Parsing nested JSON Arrays using Jackson

Im using jacson to parse the following JSON array
[
{
"target": "something",
"datapoints": [
[
null,
1482223380
]]}]
Into this POJO
public class Response {
private String target;
private List<List<Double>> datapoints;
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public List<List<Double>> getData() {
return datapoints;
}
public void setData(List<List<Double>> data) {
this.datapoints = data;
}
}
Using the following code
objectMapper.readValue(json, new TypeReference<List<Response>>() {});
This works partially, the outer list and the target is correct, however datapoints is null.
My initial solution is taken from this answere.
My question is, why are not datapoints not parsed as expected? Do this has something todo with the null values inside the array?
You could write a custom JsonDeserializer for the datapoints field.
class MyDatapointsDeserializer extends JsonDeserializer<List<List<Double>>> {
private static final TypeReference<List<List<Double>>> TYPE_REF =
new TypeReference<List<List<Double>>>() {};
#Override
public List<List<Double>> deserialize(
JsonParser jp, DeserializationContext ctxt) throws IOException {
return jp.readValueAs(TYPE_REF);
}
}
Then annotate the field accordingly.
#JsonDeserialize(using = MyDatapointsDeserializer.class)
private List<List<Double>> datapoints;

Gson serialize null for specific class or field

I want to serialize nulls for a specific field or class.
In GSON, the option serializeNulls() applies to the whole JSON.
Example:
class MainClass {
public String id;
public String name;
public Test test;
}
class Test {
public String name;
public String value;
}
MainClass mainClass = new MainClass();
mainClass.id = "101"
// mainClass has no name.
Test test = new Test();
test.name = "testName";
test.value = null;
mainClass.test = test;
Creating JSON using GSON:
GsonBuilder builder = new GsonBuilder().serializeNulls();
Gson gson = builder.create();
System.out.println(gson.toJson(mainClass));
Current ouput:
{
"id": "101",
"name": null,
"test": {
"name": "testName",
"value": null
}
}
Desired output:
{
"id": "101",
"test": {
"name": "testName",
"value": null
}
}
How to achieve the desired output?
Preferred solution would have the following properties:
Do NOT serialize nulls by default,
Serialize nulls for fields with a specific annotation.
I have a solution similar to the one of Aleksey but that can be applied to one or more fields in any class (example in Kotlin):
Create a new annotation for fields that should be serialized as null:
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FIELD)
annotation class SerializeNull
Create a TypeAdapterFactory that checks if a class has fields annotated with this annotation and removes the fields that are null and not annotated with the annotation from the JsonTree when writing the object:
class SerializableAsNullConverter : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
fun Field.serializedName() = declaredAnnotations
.filterIsInstance<SerializedName>()
.firstOrNull()?.value ?: name
val declaredFields = type.rawType.declaredFields
val nullableFieldNames = declaredFields
.filter { it.declaredAnnotations.filterIsInstance<SerializeNull>().isNotEmpty() }
.map { it.serializedName() }
val nonNullableFields = declaredFields.map { it.serializedName() } - nullableFieldNames
return if (nullableFieldNames.isEmpty()) {
null
} else object : TypeAdapter<T>() {
private val delegateAdapter = gson.getDelegateAdapter(this#SerializableAsNullConverter, type)
private val elementAdapter = gson.getAdapter(JsonElement::class.java)
override fun write(writer: JsonWriter, value: T?) {
val jsonObject = delegateAdapter.toJsonTree(value).asJsonObject
nonNullableFields
.filter { jsonObject.get(it) is JsonNull }
.forEach { jsonObject.remove(it) }
val originalSerializeNulls = writer.serializeNulls
writer.serializeNulls = true
elementAdapter.write(writer, jsonObject)
writer.serializeNulls = originalSerializeNulls
}
override fun read(reader: JsonReader): T {
return delegateAdapter.read(reader)
}
}
}
}
Register the adapter with your Gson instance:
val builder = GsonBuilder().registerTypeAdapterFactory(SerializableAsNullConverter())
And annotate the fields you would like to be nullable:
class MyClass(val id: String?, #SerializeNull val name: String?)
Serialization result:
val myClass = MyClass(null, null)
val gson = builder.create()
val json = gson.toJson(myClass)
json:
{
"name": null
}
I have interface to check when object should be serialized as null:
public interface JsonNullable {
boolean isJsonNull();
}
And the corresponding TypeAdapter (supports write only)
public class JsonNullableAdapter extends TypeAdapter<JsonNullable> {
final TypeAdapter<JsonElement> elementAdapter = new Gson().getAdapter(JsonElement.class);
final TypeAdapter<Object> objectAdapter = new Gson().getAdapter(Object.class);
#Override
public void write(JsonWriter out, JsonNullable value) throws IOException {
if (value == null || value.isJsonNull()) {
//if the writer was not allowed to write null values
//do it only for this field
if (!out.getSerializeNulls()) {
out.setSerializeNulls(true);
out.nullValue();
out.setSerializeNulls(false);
} else {
out.nullValue();
}
} else {
JsonElement tree = objectAdapter.toJsonTree(value);
elementAdapter.write(out, tree);
}
}
#Override
public JsonNullable read(JsonReader in) throws IOException {
return null;
}
}
Use it as follows:
public class Foo implements JsonNullable {
#Override
public boolean isJsonNull() {
// You decide
}
}
In the class where Foo value should be serialized as null. Note that foo value itself must be not null, otherwise custom adapter annotation will be ignored.
public class Bar {
#JsonAdapter(JsonNullableAdapter.class)
public Foo foo = new Foo();
}
For those looking for a Java version of #Joris's excellent answer, the below code should do the trick. It's largely just a translation of the Kotlin, with a minor improvement to how the serialized name of the attribute is fetched to ensure it always works when the serialized name is different than the attribute name (see the comments on the original answer).
This is the TypeAdapterFactory implementation:
public class NullableAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Field[] declaredFields = type.getRawType().getDeclaredFields();
List<String> nullableFieldNames = new ArrayList<>();
List<String> nonNullableFieldNames = new ArrayList<>();
for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(JsonNullable.class)) {
if (declaredField.getAnnotation(SerializedName.class) != null) {
nullableFieldNames.add(declaredField.getAnnotation(SerializedName.class).value());
} else {
nullableFieldNames.add(declaredField.getName());
}
} else {
if (declaredField.getAnnotation(SerializedName.class) != null) {
nonNullableFieldNames.add(declaredField.getAnnotation(SerializedName.class).value());
} else {
nonNullableFieldNames.add(declaredField.getName());
}
}
}
if (nullableFieldNames.size() == 0) {
return null;
}
TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(NullableAdapterFactory.this, type);
TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
JsonObject jsonObject = delegateAdapter.toJsonTree(value).getAsJsonObject();
for (String name: nonNullableFieldNames) {
if (jsonObject.has(name) && jsonObject.get(name) instanceof JsonNull) {
jsonObject.remove(name);
}
}
boolean originalSerializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
elementAdapter.write(out, jsonObject);
out.setSerializeNulls(originalSerializeNulls);
}
#Override
public T read(JsonReader in) throws IOException {
return delegateAdapter.read(in);
}
};
}
}
And this is the #JsonNullable annotation to mark the target attributes:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface JsonNullable {
}
I implemented it as an #JsonAdapter(NullableAdapterFactory.class) annotation on the object class, rather registering it as a TypeAdapterFactory on the GsonBuilder instance, so my object classes looked a bit like this:
#JsonAdapter(NullableAdapterFactory.class)
public class Person {
public String firstName;
public String lastName;
#JsonNullable
public String someNullableInfo;
}
However, the other approach should work just as well with this code if preferred.
Create subclass of com.google.gson.TypeAdapter and register it for required field using annotation com.google.gson.annotations.JsonAdapter. Or register it using GsonBuilder.registerTypeAdapter. In that adapter write (and read) should be implemented. For example:
public class JsonTestNullableAdapter extends TypeAdapter<Test> {
#Override
public void write(JsonWriter out, Test value) throws IOException {
out.beginObject();
out.name("name");
out.value(value.name);
out.name("value");
if (value.value == null) {
out.setSerializeNulls(true);
out.nullValue();
out.setSerializeNulls(false);
} else {
out.value(value.value);
}
out.endObject();
}
#Override
public Test read(JsonReader in) throws IOException {
in.beginObject();
Test result = new Test();
in.nextName();
if (in.peek() != NULL) {
result.name = in.nextString();
} else {
in.nextNull();
}
in.nextName();
if (in.peek() != NULL) {
result.value = in.nextString();
} else {
in.nextNull();
}
in.endObject();
return result;
}
}
in MainClass add JsonAdapter annotation with the adapter to Test class field:
public static class MClass {
public String id;
public String name;
#JsonAdapter(JsonTestNullableAdapter.class)
public Test test;
}
the output of System.out.println(new Gson.toJson(mainClass)) is:
{
"id": "101",
"test": {
"name": "testName",
"value": null
}
}
I took a few ideas from various answers here.
this implementation:
lets you choose at runtime, whether the JSON is
null
happens when JsonNullable.isJsonNull() == true
not null
happens when JsonNullable.isJsonNull() == false
omitted from the JSON (useful for HTTP PATCH requests)
happens field in Parent containing JsonNullable is null
no annotations needed
properly delegates unhandled work to a delegateAdapter by using a TypeAdapterFactory
objects that may need to be serialized to null implement this interface
/**
* [JsonNullableTypeAdapterFactory] needs to be registered with the [com.google.gson.Gson]
* serializing implementations of [JsonNullable] for [JsonNullable] to work.
*
* [JsonNullable] allows objects to choose at runtime whether they should be serialized as "null"
* serialized normally, or be omitted from the JSON output from [com.google.gson.Gson].
*
* when [isJsonNull] returns true, the subclass will be serialized to a [com.google.gson.JsonNull].
*
* when [isJsonNull] returns false, the subclass will be serialized normally.
*/
interface JsonNullable {
/**
* return true to have the entire object serialized as `null` during JSON serialization.
* return false to have this object serialized normally.
*/
fun isJsonNull(): Boolean
}
type adapter factory that serializes values to null
class JsonNullableTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
return object : TypeAdapter<T>() {
private val delegateAdapter = gson.getDelegateAdapter(this#JsonNullableTypeAdapterFactory, type)
override fun read(reader: JsonReader): T = delegateAdapter.read(reader)
override fun write(writer: JsonWriter, value: T?) {
if (value is JsonNullable && value.isJsonNull()) {
val originalSerializeNulls = writer.serializeNulls
writer.serializeNulls = true
writer.nullValue()
writer.serializeNulls = originalSerializeNulls
} else {
delegateAdapter.write(writer, value)
}
}
}
}
}
register thr type adapter factroy with GSON
new GsonBuilder()
// ....
.registerTypeAdapterFactory(new JsonNullableTypeAdapterFactory())
// ....
.create();
example object that gets serialized to JSON
data class Parent(
val hello: Child?,
val world: Child?
)
data class Child(
val name: String?
) : JsonNullable {
override fun isJsonNull(): Boolean = name == null
}
Adding to the answer given by #Arvoreniad
The two additions are resetting the null serialization state in the JsonWriter after setting to true for the output and to use the field naming policy from Gson for getting the field name.
public class SerializeNullTypeAdapterFactory implements TypeAdapterFactory {
/**
* {#inheritDoc}
*/
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Field[] declaredFields = type.getRawType().getDeclaredFields();
List<String> nullableFields = new ArrayList<>();
List<String> nonNullableFields = new ArrayList<>();
FieldNamingStrategy fieldNamingStrategy = gson.fieldNamingStrategy();
for (Field declaredField : declaredFields) {
// The Gson FieldNamingStrategy will handle the #SerializedName annotation + casing conversions
final String fieldName = fieldNamingStrategy.translateName(declaredField);
if (declaredField.isAnnotationPresent(JsonNullable.class)) {
nullableFields.add(fieldName);
} else {
nonNullableFields.add(fieldName);
}
}
if (nullableFields.isEmpty()) {
return null;
}
TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
JsonObject jsonObject = delegateAdapter.toJsonTree(value).getAsJsonObject();
nonNullableFields.forEach((var name) -> {
if (jsonObject.has(name) && (jsonObject.get(name) instanceof JsonNull)) {
jsonObject.remove(name);
}
});
boolean serializeNulls = out.getSerializeNulls();
out.setSerializeNulls(true);
elementAdapter.write(out, jsonObject);
// Reset default (in case JsonWriter is reused)
out.setSerializeNulls(serializeNulls);
}
#Override
public T read(JsonReader in) throws IOException {
return delegateAdapter.read(in);
}
};
}
}

Categories