Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read JsonAdapter Annotation From Parent Inteface #1370

Open
Singleton06 opened this issue Aug 15, 2018 · 4 comments
Open

Read JsonAdapter Annotation From Parent Inteface #1370

Singleton06 opened this issue Aug 15, 2018 · 4 comments

Comments

@Singleton06
Copy link

This is sort of a feature request and sort of just asking for other approaches of potentially handling this.

There are a couple of design choices that we have made for specific reason that I won't go into, but there are a few things to note about our use case:

  • We use interfaces for our model objects
  • It is feasible that a consumer of our API might provide their own implementation of that model object or use a default one that we provide

For this reason, serialization has always been a bit difficult because the interface is not a reliable contract for serialization (what are the default implementation field names, etc.) and those likely wouldn't work with other implementations of the model object, etc.

For that reason, I was hoping to provide on the interface a default TypeAdapter that serializes to the default instance of the object. This would allow for implementations of the interface to be able to get back to an instance of the object if they didn't have any additional logic that they were wanting.

I looked at JsonAdapter, and it seems to provide what I'm looking for, it just does not get pulled from the interface.

import java.io.IOException;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class TestScenario
{
    @JsonAdapter(ATypeAdapter.class)
    public interface A
    {
        String getValue();
    }

    public static class B implements A
    {
        @Override
        public String getValue()
        {
            return "B class";
        }
    }

    @JsonAdapter(ATypeAdapter.class)
    public static class C implements A
    {
        @Override
        public String getValue()
        {
            return "C class";
        }
    }

    public class ATypeAdapter extends TypeAdapter<A>
    {
        @Override
        public void write(JsonWriter out, A value) throws IOException
        {
            System.out.println(value.getValue());
        }

        @Override
        public A read(JsonReader in) throws IOException
        {
            return null;
        }
    }

    public static void main(String[] args)
    {
        Gson gson = new Gson();
        gson.toJson(new B()); // does not print
        gson.toJson(new C()); // does print
    }
}

In the above example, B class shows that the JsonAdapter annotation is not getting pulled from the interface. The C class shows that the annotation and the TypeAdapter do work, but only if it's on the implementation object.

Basically, I'm looking for a way to default in the serialization of all implementations of an interface unless the consumer chooses to specifically override it (which could be probable if they are implementing multiple interfaces).

@lyubomyr-shaydariv
Copy link
Contributor

What if you add a custom type adapter factory that searches for @JsonAdapter in (parent) classes interfaces? For example,

final class InterfaceJsonAdapterTypeAdapterFactory
		implements TypeAdapterFactory {

	private static final TypeAdapterFactory instance = new InterfaceJsonAdapterTypeAdapterFactory();

	private static final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();

	private InterfaceJsonAdapterTypeAdapterFactory() {
	}

	static TypeAdapterFactory get() {
		return instance;
	}

	@Override
	public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
		final Class<? super T> rawType = typeToken.getRawType();
		for ( Class<?> c = rawType; c != null && c != Object.class; c = c.getSuperclass() ) {
			if ( c.getAnnotation(JsonAdapter.class) != null ) {
				return null;
			}
			for ( final Class<?> i : c.getInterfaces() ) {
				@Nullable
				final JsonAdapter interfaceJsonAdapter = i.getAnnotation(JsonAdapter.class);
				if ( interfaceJsonAdapter != null ) {
					return adaptTypeAdapterCandidate(gson, typeToken, interfaceJsonAdapter);
				}
			}
		}
		return null;
	}

	/**
	 * @see com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory#getTypeAdapter(ConstructorConstructor, Gson, TypeToken, JsonAdapter)
	 */
	private static <T> TypeAdapter<T> adaptTypeAdapterCandidate(final Gson gson, final TypeToken<T> typeToken, final JsonAdapter jsonAdapter) {
		final Object candidate = createTypeAdapterCandidate(jsonAdapter);
		final TypeAdapter<T> typeAdapter;
		if ( candidate instanceof TypeAdapter ) {
			@SuppressWarnings("unchecked")
			final TypeAdapter<T> castInstance = (TypeAdapter<T>) candidate;
			typeAdapter = castInstance;
		} else if ( candidate instanceof TypeAdapterFactory ) {
			typeAdapter = ((TypeAdapterFactory) candidate).create(gson, typeToken);
		} else if ( candidate instanceof JsonSerializer || candidate instanceof JsonDeserializer ) {
			@SuppressWarnings("unchecked")
			final JsonSerializer<T> serializer = candidate instanceof JsonSerializer ? (JsonSerializer<T>) candidate : null;
			@SuppressWarnings("unchecked")
			final JsonDeserializer<T> deserializer = candidate instanceof JsonDeserializer ? (JsonDeserializer<T>) candidate : null;
			typeAdapter = new TreeTypeAdapter<>(serializer, deserializer, gson, typeToken, null);
		} else {
			throw new IllegalArgumentException("Cannot adapt " + candidate);
		}
		return typeAdapter != null && jsonAdapter.nullSafe() ? typeAdapter.nullSafe() : typeAdapter;
	}

	private static Object createTypeAdapterCandidate(final JsonAdapter jsonAdapter) {
		try {
			return unsafeAllocator.newInstance(jsonAdapter.value());
		} catch ( final Exception ex ) {
			throw new RuntimeException(ex);
		}
	}

}
final Gson gson = new GsonBuilder()
		.registerTypeAdapterFactory(InterfaceJsonAdapterTypeAdapterFactory.get())
		.create();

Not well-tested.

@Singleton06
Copy link
Author

This is an interesting idea, and one that I will explore a little more, though, I honestly just wish that it was built into gson to be able to look this information up and use it so that I don't have to give additional instructions for consumers to register a type adapter or type adapter factory.

@JakeWharton
Copy link
Contributor

What happens when there's two interfaces with two json adapters? What happens when the interface is from a library and you want to serialize it differently? What happens when the interface and the class are from a library and you want to serialize it differently?

I don't think adding this behavior would be a good idea.

@Singleton06
Copy link
Author

Singleton06 commented Aug 28, 2018

What happens when the interface is from a library and you want to serialize it differently?
What happens when the interface and the class are from a library and you want to serialize it differently?

I think that it works as long as you always allow the ability to override, which would maintain the behavior of how it is currently implemented with a concrete class. If you provide the annotation, you can still override it with specific Gson configuration (through GsonBuilder).

There is also a lot of power here in that common libraries could allow for certain data to be serialized by gson by default and not have multiple consumers take on that burden unless they specifically want to. This allows for a more consistent serialization and higher code re-use.

What happens when there's two interfaces with two json adapters?

This is probably the justification for not doing it that would make the most sense to me since it is very difficult to come up with the right behavior for this. I would even be in favor of opting out of using the JsonAdapter annotation if the target class implements multiple interfaces or something like that, and I feel that it would be a justifiable instance to not use the JsonAdapter value since GSON would not be able to determine which one to use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants