简体   繁体   中英

JSON serialize only base class fields when a base class-typed reference is passed

I am using the Gson library. What is a clean / idiomatic way to ask Gson to serialize only the fields of the base class when the object give to Gson is of the base class type? Note that this is different from similar questions (eg this one ) which ask how to always exclude specific fields. In my use case I only want the inherited class fields excluded when the Gson library is passed a derived class object through a base class-typed reference. Otherwise, ie if the Gson library is passed a derived class object through a derived-class-typed reference then I want the fields to appear in the serialization.

SSCCE follows:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

class A {
    public int a;
    public A(final int a) {
        this.a = a;
    }
}

class B extends A {
    public int b;

    public B(final int a, final int b) {
        super(a);
        this.b = b;
    }

}

public class Main {

    public static void main(String args[]) {

        final A a = new B(42, 142);
        final Gson gson = new GsonBuilder().serializeNulls().create();

        System.out.printf("%s\n", gson.toJson(a));
    }
}

The above prints:

{"b":142,"a":42}

I am looking for a clean way to make it print:

{"a":42}

However if the following code is used:

final B b = new B(42, 142);

... then I want gson.toJson(b) to indeed return:

{"b":142,"a":42}

Is there a clean way to achieve that?

UPDATE

The accepted answer at the time of this writing suggests using toJson(o, A.class) which does work in this case. However, it appears that this method does not scale well to generics. Eg:

class A {
    public int a;
    public A(final int a) {
        this.a = a;
    }
}

class B extends A {
    public int b;

    public B(final int a, final int b) {
        super(a);
        this.b = b;
    }
}

class Holder<T> {
    public final T t;

    public Holder(final T t) {
        this.t = t;
    }
}

final A a = new B(42, 142);
final Holder<A> holder = new Holder<A>(a);
final Gson gson = new GsonBuilder().serializeNulls().create();

final Type TYPE= new TypeToken<Holder<A>>() {}.getType();
System.out.printf("%s\n", gson.toJson(holder, TYPE));

Regrettably, the above prints:

{"t":{"b":142,"a":42}}

There appears to be no straight forward configuration for this in Gson user manual. But there is something worth trying:

You can use the Gson serializer to use a dynamic ExclusionStrategy . Here, "Dynamic" strategy means every time you need to serialize a Class Foo , you'll need a Gson instance configured with OnlyFieldsFor<Foo> startegy.

ExclusionStrategy is an interface, which specifies 2 methods:

  1. shouldSkipClass() - Use this to filter out classes you don't want.

  2. shouldSkipField() - This is useful. It is called with FieldAttributes , which has information about the class the fields was declared in. Return false if the fields was not declared in the base class.

Is there a clean way to achieve that?

Yes.

You don't need exclusion strategies despite they are flexible enough to cover such a scenario. Data bag classes serialization and deserialization is driven by TypeAdapter implementation found in ReflectiveTypeAdapterFactory . This type adapter takes the most concrete class fields into account.

Having that said, the only thing you need is specifying the most concrete type to take fields from:

final Object o = new B(42, 142);
System.out.println(gson.toJson(o, A.class));

The reason of why it was not working for you is that your code specified the most concrete class implicitly , just like if you'd use gson.toJson(o, o.getClass()) or gson.toJson(o, B.class) for the code above.

The generic types problem

I'm afraid ReflectiveTypeAdapterFactory can't take type parameters into account for such a scenario. Not sure what the getRuntimeTypeIfMoreSpecific method exactly does and what it's original purpose, but it "converts" the A type to the concrete B type because of the type instanceof Class<?> check that allows to overwrite the field type with type = value.getClass(); . It looks like you must implement a workaround for such a case:

.registerTypeAdapterFactory(new TypeAdapterFactory() {
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        final Class<? super T> rawType = typeToken.getRawType();
        if ( rawType != Holder.class ) {
            return null;
        }
        final Type valueType = ((ParameterizedType) typeToken.getType()).getActualTypeArguments()[0];
        @SuppressWarnings({"unchecked", "rawtypes"})
        final TypeAdapter<Object> valueTypeAdapter = (TypeAdapter) gson.getDelegateAdapter(this, TypeToken.get(valueType));
        final TypeAdapter<Holder<?>> typeAdapter = new TypeAdapter<Holder<?>>() {
            @Override
            public void write(final JsonWriter out, final Holder<?> value)
                    throws IOException {
                out.beginObject();
                out.name("t");
                valueTypeAdapter.write(out, value.t);
                out.endObject();
            }

            @Override
            public Holder<?> read(final JsonReader in)
                    throws IOException {
                Object t = null;
                in.beginObject();
                while ( in.hasNext() ) {
                    final String name = in.nextName();
                    switch ( name ) {
                    case "t":
                        t = valueTypeAdapter.read(in);
                        break;
                    default:
                        // do nothing;
                        break;
                    }
                }
                in.endObject();
                return new Holder<>(t);
            }
        }.nullSafe();
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
        return castTypeAdapter;
    }
})

Unfortunately having this type adapter in your Gson instance makes the @SerializedName , @JsonAdapter and other cool things unavailable and makes it stick to hardcoded field names. This is probably a Gson bug.

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