42

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.
Eugen Pechanec
  • 37,669
  • 7
  • 103
  • 124
Martin
  • 481
  • 1
  • 5
  • 14
  • @DatoMumladze i updated my question – Martin Feb 18 '16 at 09:32
  • I could not find this feature in Gson. Here is some interesting [link](https://mindfirejavaexperts.wordpress.com/2014/03/14/how-to-use-gson-library/) Or you can use Jackson to serialize object to json and exclude null values for specific fields using this annotation `@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)` – Davit Mumladze Feb 18 '16 at 11:28
  • Possible duplicate of [Excluding certain fields from Serialization based on value in GSON](http://stackoverflow.com/questions/13120354/excluding-certain-fields-from-serialization-based-on-value-in-gson) – tima May 22 '17 at 00:22

6 Answers6

19

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
}
Eric
  • 16,397
  • 8
  • 68
  • 76
Joris
  • 1,437
  • 1
  • 15
  • 22
  • 2
    Thanks for this! I reimplemented it in a Java project using Retrofit, it's the best solution I've found for customizable nullable types in GSON. Note that at least in Java, you'll need to pull the serialized field name from `declaredField.getAnnotation(SerializedName.class).value()` in order for this to work for more complex variable names. – Arvoreniad Mar 23 '20 at 19:22
  • Interesting @Arvoreniad, I've tested this case with the Kotlin implementation and no additional logic is needed to retain the serialized name. The delegateAdapter should make sure of that. – Joris Apr 23 '20 at 13:47
  • I was using Kotlin on Android, and I noticed a the same thing that @Arvoreniad did. Fields with a @ SerializedName annotation, if null, are sarialized as null in the produced JSON. – Eric May 12 '20 at 10:19
  • 1
    @Arvoreniad Can you post your Java solution? – Zookey Jun 17 '20 at 09:11
  • @Zookey I just posted the solution as an additional answer. – Arvoreniad Jun 17 '20 at 15:05
  • this ignores nested json. any idea why? – Raghunandan Feb 22 '22 at 03:57
6

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();
}
Oleksii Masnyi
  • 2,922
  • 2
  • 22
  • 27
  • The performance of this solution is quite bad because it creates a JsonElement tree as intermediate data structure instead of writing directly to the JsonWriter. – Miro Spönemann Apr 12 '18 at 11:13
  • @MiroSpönemann if I recall correctly, `objectAdapter.toJsonTree` is used when the object should be serialized into a normal json. – Oleksii Masnyi Apr 12 '18 at 14:49
  • `toJsonTree()` is used to create a tree of JsonElements. It is much faster to write directly to the JsonWriter without creating an intermediate data structure. The generic Gson class that does this is ReflectiveTypeAdapterFactory. – Miro Spönemann Apr 16 '18 at 07:58
  • You could try to write a TypeAdapterFactory and obtain the correct type adapter with `gson.getDelegateAdapter(...)`; see the JavaDoc of that method for for more information. – Miro Spönemann Apr 16 '18 at 08:03
5

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.

Charles
  • 3
  • 1
  • 2
Arvoreniad
  • 510
  • 5
  • 10
0

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
    }
}
0

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
}
Eric
  • 16,397
  • 8
  • 68
  • 76
0

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);
            }
        };
    }
}
jxbadam
  • 11
  • 1