简体   繁体   中英

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);
            }
        };
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM