Repository: square/retrofit
Issue: Add converter for scalars to response body
Labels: Enhancement


Issue comments:
I'm not sure there's enough of a use case to warrant doing this. Why do you want it?

I made a scalar response body converter to deal with a legacy API that mixes application/json and text/plain. I'm happy to make a pull request if this is a common use case.

I have exactly the same case described by @connorMathews. Also it will make the library "converter-scalars" more general. Thanks.

You can use ResponseBody and call .string().

On Mon, Jan 11, 2016, 10:33 AM Connor notifications@github.com wrote:

> I made a scalar response body converter to deal with a legacy API that
> mixes application/json and text/plain. I'm happy to make a pull request if
> this is a common use case.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/square/retrofit/issues/1459#issuecomment-170645686.

I have also GsonConverterFactory for application/json responses:
        Retrofit.Builder builder = new Retrofit.Builder()
                .baseUrl(sServiceUrl)
                .callFactory(getOkHttpClient())
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson));
And when I get a text/plain response it gets MalformedJsonException. 

Use ResponseBody for those.

Yep, a converter just moves the .string() call out of the calling code. In my case, I have endpoints that return integer item ids and others that return strings. I wanted to make the distinction explicitly in the service interface definition. I think type safety is a compelling reason but I understand if this isn't common enough to include in the project.

I'm fine with the change. Feel free to make a PR, otherwise I'll get to it before 2.0 final.

This was merged in.


Pull request review comments:
Copy license header from another file. Use 2016 for the date.

I'm thinking that this should assert that the string is exactly 1 character and throw an `IOException` for a non-1 length. And a test case for both the 0 and non-1 lengths.

Actually I'll just make this change locally during the merge.

:+1: 


Patch that resolved the issue:
diff --git a/retrofit-converters/scalars/src/main/java/retrofit2/ScalarResponseBodyConverters.java b/retrofit-converters/scalars/src/main/java/retrofit2/ScalarResponseBodyConverters.java
new file mode 100644
index 0000000000..5562dab554
--- /dev/null
+++ b/retrofit-converters/scalars/src/main/java/retrofit2/ScalarResponseBodyConverters.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package retrofit2;
+
+import java.io.IOException;
+import okhttp3.ResponseBody;
+
+final class ScalarResponseBodyConverters {
+  private ScalarResponseBodyConverters() {
+  }
+
+  static final class StringResponseBodyConverter implements Converter<ResponseBody, String> {
+    static final StringResponseBodyConverter INSTANCE = new StringResponseBodyConverter();
+
+    @Override public String convert(ResponseBody value) throws IOException {
+      return value.string();
+    }
+  }
+
+  static final class BooleanResponseBodyConverter implements Converter<ResponseBody, Boolean> {
+    static final BooleanResponseBodyConverter INSTANCE = new BooleanResponseBodyConverter();
+
+    @Override public Boolean convert(ResponseBody value) throws IOException {
+      return Boolean.valueOf(value.string());
+    }
+  }
+
+  static final class ByteResponseBodyConverter implements Converter<ResponseBody, Byte> {
+    static final ByteResponseBodyConverter INSTANCE = new ByteResponseBodyConverter();
+
+    @Override public Byte convert(ResponseBody value) throws IOException {
+      return Byte.valueOf(value.string());
+    }
+  }
+
+  static final class CharacterResponseBodyConverter implements Converter<ResponseBody, Character> {
+    static final CharacterResponseBodyConverter INSTANCE = new CharacterResponseBodyConverter();
+
+    @Override public Character convert(ResponseBody value) throws IOException {
+      return value.string().charAt(0);
+    }
+  }
+
+  static final class DoubleResponseBodyConverter implements Converter<ResponseBody, Double> {
+    static final DoubleResponseBodyConverter INSTANCE = new DoubleResponseBodyConverter();
+
+    @Override public Double convert(ResponseBody value) throws IOException {
+      return Double.valueOf(value.string());
+    }
+  }
+
+  static final class FloatResponseBodyConverter implements Converter<ResponseBody, Float> {
+    static final FloatResponseBodyConverter INSTANCE = new FloatResponseBodyConverter();
+
+    @Override public Float convert(ResponseBody value) throws IOException {
+      return Float.valueOf(value.string());
+    }
+  }
+
+  static final class IntegerResponseBodyConverter implements Converter<ResponseBody, Integer> {
+    static final IntegerResponseBodyConverter INSTANCE = new IntegerResponseBodyConverter();
+
+    @Override public Integer convert(ResponseBody value) throws IOException {
+      return Integer.valueOf(value.string());
+    }
+  }
+
+  static final class LongResponseBodyConverter implements Converter<ResponseBody, Long> {
+    static final LongResponseBodyConverter INSTANCE = new LongResponseBodyConverter();
+
+    @Override public Long convert(ResponseBody value) throws IOException {
+      return Long.valueOf(value.string());
+    }
+  }
+
+  static final class ShortResponseBodyConverter implements Converter<ResponseBody, Short> {
+    static final ShortResponseBodyConverter INSTANCE = new ShortResponseBodyConverter();
+
+    @Override public Short convert(ResponseBody value) throws IOException {
+      return Short.valueOf(value.string());
+    }
+  }
+}
diff --git a/retrofit-converters/scalars/src/main/java/retrofit2/ScalarsConverterFactory.java b/retrofit-converters/scalars/src/main/java/retrofit2/ScalarsConverterFactory.java
index 5402455dab..3acc527902 100644
--- a/retrofit-converters/scalars/src/main/java/retrofit2/ScalarsConverterFactory.java
+++ b/retrofit-converters/scalars/src/main/java/retrofit2/ScalarsConverterFactory.java
@@ -18,6 +18,16 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import okhttp3.RequestBody;
+import okhttp3.ResponseBody;
+import retrofit2.ScalarResponseBodyConverters.BooleanResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.ByteResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.CharacterResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.DoubleResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.FloatResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.IntegerResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.LongResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.ShortResponseBodyConverter;
+import retrofit2.ScalarResponseBodyConverters.StringResponseBodyConverter;
 
 /**
  * A {@linkplain Converter.Factory converter} for strings and both primitives and their boxed types
@@ -55,4 +65,37 @@ public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] an
     }
     return null;
   }
+
+  @Override
+  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
+      Retrofit retrofit) {
+    if (type == String.class) {
+      return StringResponseBodyConverter.INSTANCE;
+    }
+    if (type == Boolean.class) {
+      return BooleanResponseBodyConverter.INSTANCE;
+    }
+    if (type == Byte.class) {
+      return ByteResponseBodyConverter.INSTANCE;
+    }
+    if (type == Character.class) {
+      return CharacterResponseBodyConverter.INSTANCE;
+    }
+    if (type == Double.class) {
+      return DoubleResponseBodyConverter.INSTANCE;
+    }
+    if (type == Float.class) {
+      return FloatResponseBodyConverter.INSTANCE;
+    }
+    if (type == Integer.class) {
+      return IntegerResponseBodyConverter.INSTANCE;
+    }
+    if (type == Long.class) {
+      return LongResponseBodyConverter.INSTANCE;
+    }
+    if (type == Short.class) {
+      return ShortResponseBodyConverter.INSTANCE;
+    }
+    return null;
+  }
 }


Test patch:
diff --git a/retrofit-converters/scalars/src/test/java/retrofit2/ScalarsConverterFactoryTest.java b/retrofit-converters/scalars/src/test/java/retrofit2/ScalarsConverterFactoryTest.java
index 0fe8b9be1f..d6b2146d7e 100644
--- a/retrofit-converters/scalars/src/test/java/retrofit2/ScalarsConverterFactoryTest.java
+++ b/retrofit-converters/scalars/src/test/java/retrofit2/ScalarsConverterFactoryTest.java
@@ -24,6 +24,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import retrofit2.http.Body;
+import retrofit2.http.GET;
 import retrofit2.http.POST;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -50,6 +51,18 @@ interface Service {
       @POST("/") Call<ResponseBody> longObject(@Body Long body);
       @POST("/") Call<ResponseBody> shortPrimitive(@Body short body);
       @POST("/") Call<ResponseBody> shortObject(@Body Short body);
+
+      @GET("/") Call<Object> object();
+
+      @GET("/") Call<String> stringObject();
+      @GET("/") Call<Boolean> booleanObject();
+      @GET("/") Call<Byte> byteObject();
+      @GET("/") Call<Character> charObject();
+      @GET("/") Call<Double> doubleObject();
+      @GET("/") Call<Float> floatObject();
+      @GET("/") Call<Integer> integerObject();
+      @GET("/") Call<Long> longObject();
+      @GET("/") Call<Short> shortObject();
   }
 
   @Rule public final MockWebServer server = new MockWebServer();
@@ -64,7 +77,7 @@ interface Service {
     service = retrofit.create(Service.class);
   }
 
-  @Test public void unsupportedTypesNotMatched() {
+  @Test public void unsupportedRequestTypesNotMatched() {
     try {
       service.object(null);
       fail();
@@ -79,7 +92,7 @@ interface Service {
     }
   }
 
-  @Test public void supportedTypes() throws IOException, InterruptedException {
+  @Test public void supportedRequestTypes() throws IOException, InterruptedException {
     RecordedRequest request;
 
     server.enqueue(new MockResponse());
@@ -201,4 +214,56 @@ interface Service {
     assertThat(request.getHeader("Content-Length")).isEqualTo("2");
     assertThat(request.getBody().readUtf8()).isEqualTo("11");
   }
+
+  @Test public void unsupportedResponseTypesNotMatched() {
+    try {
+      service.object();
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage(
+          "Unable to create converter for class java.lang.Object\n"
+              + "    for method Service.object");
+      assertThat(e.getCause()).hasMessage(
+          "Could not locate ResponseBody converter for class java.lang.Object. Tried:\n"
+              + " * retrofit2.BuiltInConverters\n" + " * retrofit2.ScalarsConverterFactory");
+    }
+  }
+
+  @Test public void supportedResponseTypes() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setBody("test"));
+    Response<String> stringResponse = service.stringObject().execute();
+    assertThat(stringResponse.body()).isEqualTo("test");
+
+    server.enqueue(new MockResponse().setBody("true"));
+    Response<Boolean> booleanResponse = service.booleanObject().execute();
+    assertThat(booleanResponse.body()).isTrue();
+
+    server.enqueue(new MockResponse().setBody("5"));
+    Response<Byte> byteResponse = service.byteObject().execute();
+    assertThat(byteResponse.body()).isEqualTo((byte) 5);
+
+    server.enqueue(new MockResponse().setBody("b"));
+    Response<Character> characterResponse = service.charObject().execute();
+    assertThat(characterResponse.body()).isEqualTo('b');
+
+    server.enqueue(new MockResponse().setBody("13.13"));
+    Response<Double> doubleResponse = service.doubleObject().execute();
+    assertThat(doubleResponse.body()).isEqualTo(13.13);
+
+    server.enqueue(new MockResponse().setBody("13.13"));
+    Response<Float> floatResponse = service.floatObject().execute();
+    assertThat(floatResponse.body()).isEqualTo(13.13f);
+
+    server.enqueue(new MockResponse().setBody("13"));
+    Response<Integer> integerResponse = service.integerObject().execute();
+    assertThat(integerResponse.body()).isEqualTo(13);
+
+    server.enqueue(new MockResponse().setBody("1347"));
+    Response<Long> longResponse = service.longObject().execute();
+    assertThat(longResponse.body()).isEqualTo(1347L);
+
+    server.enqueue(new MockResponse().setBody("134"));
+    Response<Short> shortResponse = service.shortObject().execute();
+    assertThat(shortResponse.body()).isEqualTo((short) 134);
+  }
 }