2

I need to send HTTP PUT request with custom JSON object in one of request's parameter. Here is a problem: when I use it with Retrofit2, my serializer doesn't called.

My object should looks like this:

{
    "ignore":["item1", "item2"]
}

When I call it directly it works well:

final Gson gson = new GsonBuilder()
        .registerTypeAdapter(MyModel.class, new MyModelSerializer())
        .create();
String s = gson.toJson(new MyModel(MyModel.ActionName.ACCEPT, new ArrayList<String>()));
Log.d("tag", s);

I get {"accept":[]}. But when I call it with Retrofit I see in logs this: D/OkHttp: name=abc&items=ru.bartwell.myapp.MyModel%4010a3d8b

I make request with this code:

try {
    MyModel myModel = new MyModel(MyModel.ActionName.ACCEPT, new ArrayList<String>());
    Response<ResultModel> response = getMyApi().getResult(1, "abc", myModel).execute();
    if (response.isSuccessful()) {
        ResultModel resultModel = response.body();
        // handle resultModel
    }
} catch (Exception e) {
    e.printStackTrace();
}

MyApi:

@FormUrlEncoded
@PUT("path/{id}")
Call<ResultModel> getResult(@Path("id") long item, @Field("name") @NonNull String name, @Field("items") @NonNull MyModel myModel);

getMyApi() method:

public static MyApi getMyApi() {
    final Gson gson = new GsonBuilder()
        .registerTypeAdapter(MyModel.class, new MyModelSerializer())
        .create();

    return new Retrofit.Builder()
            .baseUrl(BuildConfig.END_POINT_MY_API)
            .client(MyOkHttpClient.create())
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build()
            .create(MyApi.class);
}

Model:

public class MyModel {
    @NonNull
    private ActionName mActionName;
    @NonNull
    private List<String> mItems = new ArrayList<>();

    public MyModel(@NonNull final ActionName actionName, @NonNull final List<String> items) {
        mActionName = actionName;
        mItems = items;
    }

    @NonNull
    public ActionName getActionName() {
        return mActionName;
    }

    @NonNull
    public List<String> getItems() {
        return mItems;
    }

    public enum ActionName {
        ACCEPT("accept"), DECLINE("decline"), IGNORE("ignore");

        private String mName;

        ActionName(@NonNull final String name) {
            mName = name;
        }

        public String getName() {
            return mName;
        }
    }
}

Serializer:

public class MyModelSerializer implements JsonSerializer<MyModel> {
    @Override
    public JsonElement serialize(@NonNull MyModel src, @NonNull Type typeOfSrc, @NonNull JsonSerializationContext context) {
        JsonObject obj = new JsonObject();
        obj.add(src.getActionName().getName(), new Gson().toJsonTree(src.getItems()));
        return obj;
    }
}

How to fix it?

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
BArtWell
  • 4,176
  • 10
  • 63
  • 106

1 Answers1

1

This happens because @Field-annotated parameters are serialized with stringConverter. To override the default behavior, you just need to add another Converter.Factory with the stringConverter method overridden:

.addConverterFactory(new Converter.Factory() {
    @Override
    public Converter<?, String> stringConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
        // Example check for supported classes
        if ( type instanceof Class ) {
            final Class<?> clazz = (Class<?>) type;
            if ( MyModel.class.isAssignableFrom(clazz) ) {
                return (Converter<Object, String>) value -> gson.toJson(value, type);
            }
        }
        return super.stringConverter(type, annotations, retrofit);
    }
})

Then your query parameter string will look like this:

name=abc&items=%7B%22accept%22%3A%5B%5D%7D

(this %7B%22accept%22%3A%5B%5D%7D' is URI-encoded {"accept":[]}).

A few side-notes:

  • Gson instances are thread-safe and can be created once per the whole application (And this is perfectly fine to pass the single instance to all your components).
  • Your MyModelSerializer can be improved: you should not create a Gson instance there since deserializers are bound to the Gson instance configuration, so you have to delegate the serialization via the given context like context.serialize(myModel.mItems, itemsType), where itemsType is new TypeToken<List<String>>() {}.getType().
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105