Search code examples
androidgenericsretrofitretrofit2

Retrofit generic service interface


I am creating a generic API layer for Retrofit

Here is my service class:

public interface ApiService {

    @POST("api/authenticate")
    Call<Class> postData(@Body Class postBody);

}

public  void  postRequest(String actionUrl,GenericModelClass postBodyModel){
    mApiService.postData(postBodyModel.getClass()).enqueue(new Callback<Class>() {


        @Override
        public void onResponse(Call<Class> call, Response<Class> response) {
            response.getClass().getComponentType();

            Log.d("TFFF", response.toString());
        }

        @Override
        public void onFailure(Call<Class> call, Throwable t) {
          Log.d("TFFF", t.toString());
        }
    });
}

But this one gives me:

java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: a2a.rnd.com.a2ahttplibrary.retrofit.model.User. Forgot to register a type adapter?

I want to get the User type from the generic type but I am getting this exception.


Solution

  • You're doing it in a way that does not make sense, and this is why you're getting:

    java.lang.UnsupportedOperationException: Attempted to serialize java.lang.Class: a2a.rnd.com.a2ahttplibrary.retrofit.model.User. Forgot to register a type adapter?

    Your service does not specify a type parameter. Class handles quite another purpose: it's an object that represents a class loaded by JVM. Serializing and deserializing Class instances makes really tiny sense if any, and that's why Gson does not provide it. All you want is generic methods. There are myriad articles for this subject over Internet.

    Next, Retrofit does not work with method type parameters to simplify the type analysis under the hood dramatically. That's fine.

    @GET("/")
    <T> Call<T> get();
    

    This won't work. How would you pass necessary type information data then? The only way to pass that info I can think of is introducing a wrapper to hold both value and its type (or type token to simplify Gson).

    final class GenericBody<T> {
    
        final T body;
        final TypeToken<T> typeToken;
    
        GenericBody(final T body, final TypeToken<T> typeToken) {
            this.body = body;
            this.typeToken = typeToken;
        }
    
    }
    

    Then an example service might be declared as follows:

    interface IGenericService {
    
        @POST("/")
        Call<Void> post(@Body @SuppressWarnings("rawtypes") GenericBody genericBody);
    
    }
    

    Here, the Call is declared to return nothing, and genericBody is intentionally made raw-typed to let it pass Retrofit validation.

    Next, the Gson part.

    final class GenericBodyTypeAdapterFactory
            implements TypeAdapterFactory {
    
        private static final TypeAdapterFactory genericBodyTypeAdapterFactory = new GenericBodyTypeAdapterFactory();
    
        private GenericBodyTypeAdapterFactory() {
        }
    
        static TypeAdapterFactory getGenericBodyTypeAdapterFactory() {
            return genericBodyTypeAdapterFactory;
        }
    
        @Override
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            if ( !GenericBody.class.isAssignableFrom(typeToken.getRawType()) ) {
                return null;
            }
            final TypeAdapter<GenericBody<T>> genericBodyTypeAdapter = new TypeAdapter<GenericBody<T>>() {
                @Override
                public void write(final JsonWriter out, final GenericBody<T> value)
                        throws IOException {
                    final T body = value.body;
                    final TypeAdapter<T> typeAdapter = gson.getDelegateAdapter(GenericBodyTypeAdapterFactory.this, value.typeToken);
                    typeAdapter.write(out, body);
                }
    
                @Override
                public GenericBody<T> read(final JsonReader in) {
                    throw new UnsupportedOperationException();
                }
            };
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) genericBodyTypeAdapter;
            return typeAdapter;
        }
    
    }
    

    What it does it is:

    • checks if it can handle GenericBody instances;
    • resolves appropriate type adapters for the <T> by the bound type token;
    • writes the generic body value to the output.

    No read is implemented.

    Example of use (full of mocks (staticResponse(applicationJsonMediaType, "OK")) that can be easily translated to your code):

    private static final TypeToken<List<String>> stringListTypeToken = new TypeToken<List<String>>() {
    };
    
    private static final Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(getGenericBodyTypeAdapterFactory())
            .create();
    
    private static final OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(staticResponse(applicationJsonMediaType, "OK"))
            .build();
    
    private static final Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("http://whatever")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build();
    
    private static final IGenericService genericService = retrofit.create(IGenericService.class);
    
    public static void main(final String... args)
            throws IOException {
        final GenericBody<List<String>> body = new GenericBody<>(asList("foo", "bar", "baz"), stringListTypeToken);
        genericService.post(body).execute();
    }
    

    This would write ["foo","bar","baz"] to the output stream respecting properly configured Gson (de)serialization strategies.