Gson serialize null for specific class or field

后端 未结 5 420
面向向阳花
面向向阳花 2021-01-01 11:00

I want to serialize nulls for a specific field or class.

In GSON, the option serializeNulls() applies to the whole JSON.

Example:



        
相关标签:
5条回答
  • 2021-01-01 11:36

    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 讨论(0)
  • 2021-01-01 11:48

    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);
                        }
                    }
    
                    out.setSerializeNulls(true);
                    elementAdapter.write(out, jsonObject);
                }
    
                @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.

    0 讨论(0)
  • 2021-01-01 11:50

    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
    }
    
    0 讨论(0)
  • 2021-01-01 11:53

    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
    }
    
    0 讨论(0)
  • 2021-01-01 11:57

    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();
    }
    
    0 讨论(0)
提交回复
热议问题