Repository: square/retrofit
Issue: Update to Gson 2.4, use newJsonWriter in GsonConverterFactory

Hello guys!

Recently I found out issue with null fields serialization during request call process. It is more looks like issue of Gson and there is pretty easy workaround of issue. So far issue lies within [GsonConverterFactory](https://github.com/square/retrofit/blob/master/retrofit-converters/gson/src/main/java/retrofit/GsonConverterFactory.java#L60) which internally uses [TypeAdapter](https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/TypeAdapter.html) instead of actual gson instance. I am not sure what was intent of using TypeAdapter, but such approach ignores extra configurations of Gson object such as `serializaeNulls: false`.

Below is sample of code. Comment points out issue.

``` java
    class Data {
        private final String tag;
        private String optionalTag;

        private Data(@NonNull String tag) {
            this.tag = tag;
        }

        public void setOptionalTag(@Nullable String optionalTag) {
            this.optionalTag = optionalTag;
        }

        @NonNull
        public String getTag() {
            return tag;
        }

        @Nullable
        public String getOptionalTag() {
            return optionalTag;
        }
    }

    interface FooService {
        @POST("v1/something/create")
        Call<Response<Object>> barEndpoint(@Body Data data);
    }
```

``` java
   @Test
    public void test() {
        Gson gson = new GsonBuilder().create();
        Converter.Factory converterFactory = GsonConverterFactory.create(gson);
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://any.api/")
                .addConverterFactory(converterFactory)
                .build();
        Data data = new Data("my tag");
        FooService service = retrofit.create(FooService.class);

        // Expected result of serialization is {"tag": "my tag"}
        // Actual result of serialization is {"tag": "my tag", "optionalTag": null}
        service.barEndpoint(data);
   }
```

Suggested fix could be to use `gson instance` and not TypeAdapter.

``` java
class GsonConverterFactory implements Converter.Factory {
    /**
     * Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and
     * decoding from JSON (when no charset is specified by a header) will use UTF-8.
     */
    public static GsonConverterFactory create() {
        return create(new Gson());
    }

    /**
     * Create an instance using {@code gson} for conversion. Encoding to JSON and
     * decoding from JSON (when no charset is specified by a header) will use UTF-8.
     */
    public static GsonConverterFactory create(Gson gson) {
        return new GsonConverterFactory(gson);
    }

    private final Gson gson;

    private GsonConverterFactory(Gson gson) {
        if (gson == null) throw new NullPointerException("gson == null");
        this.gson = gson;
    }

    /** Create a converter for {@code type}. */
    @Override public Converter<?> get(Type type) {
        return new GsonConverter<>(type, gson);
    }

    private static class GsonConverter<T> implements Converter<T> {
        private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
        private static final Charset UTF_8 = Charset.forName("UTF-8");

        private final Gson mGson;
        private final Type mType;

        DefaultGsonConverter(Type type, Gson gson) {
            mType = type;
            mGson = gson;
        }

        @Override public T fromBody(ResponseBody body) throws IOException {
            Reader in = body.charStream();
            try {
                return mGson.fromJson(in, mType);
            } finally {
                try {
                    in.close();
                } catch (IOException ignored) {
                }
            }
        }

        @Override public RequestBody toBody(T value) {
            Buffer buffer = new Buffer();
            Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
            try {
                mGson.toJson(value, mType, writer);
                writer.flush();
            } catch (IOException e) {
                throw new AssertionError(e); // Writing to Buffer does no I/O.
            }
            return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
        }
    }
}
```


Issue comments:
Gson was changed to expose this property (https://github.com/google/gson/pull/700) so once it has its next release (this month or next, well before our v2.0) we can update to configuring a `JsonWriter` to serialize nulls based on the user configuration.

Waiting on 2.4 release which we're pushing for this coming week: https://github.com/google/gson/issues/668. If it happens quick enough I can squeeze this into beta2!

awesome, thanks!

Great, thank you!

I can't speak English well, I have a question.
like this:
status:"1",msg:"success",data:{} 
but:
status:"0",msg:"error",data""

How to solve



Pull request review comments:

Patch that resolved the issue:
diff --git a/pom.xml b/pom.xml
index 5638a73c28..40b028a8d4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,7 @@
     <rxjava.version>1.0.14</rxjava.version>
 
     <!-- Converter Dependencies -->
-    <gson.version>2.3.1</gson.version>
+    <gson.version>2.4</gson.version>
     <protobuf.version>2.5.0</protobuf.version>
     <jackson.version>2.4.3</jackson.version>
     <wire.version>1.7.0</wire.version>
diff --git a/retrofit-converters/gson/src/main/java/retrofit/GsonConverterFactory.java b/retrofit-converters/gson/src/main/java/retrofit/GsonConverterFactory.java
index 59b921be02..36279874c0 100644
--- a/retrofit-converters/gson/src/main/java/retrofit/GsonConverterFactory.java
+++ b/retrofit-converters/gson/src/main/java/retrofit/GsonConverterFactory.java
@@ -16,6 +16,8 @@
 package retrofit;
 
 import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.reflect.TypeToken;
 import com.squareup.okhttp.RequestBody;
 import com.squareup.okhttp.ResponseBody;
 import java.lang.annotation.Annotation;
@@ -55,10 +57,12 @@ private GsonConverterFactory(Gson gson) {
 
   @Override
   public Converter<ResponseBody, ?> fromResponseBody(Type type, Annotation[] annotations) {
-    return new GsonResponseBodyConverter<>(gson, type);
+    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
+    return new GsonResponseBodyConverter<>(adapter);
   }
 
   @Override public Converter<?, RequestBody> toRequestBody(Type type, Annotation[] annotations) {
-    return new GsonRequestBodyConverter<>(gson, type);
+    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
+    return new GsonRequestBodyConverter<>(gson, adapter);
   }
 }
diff --git a/retrofit-converters/gson/src/main/java/retrofit/GsonRequestBodyConverter.java b/retrofit-converters/gson/src/main/java/retrofit/GsonRequestBodyConverter.java
index a2b527eadc..651b02973f 100644
--- a/retrofit-converters/gson/src/main/java/retrofit/GsonRequestBodyConverter.java
+++ b/retrofit-converters/gson/src/main/java/retrofit/GsonRequestBodyConverter.java
@@ -16,12 +16,13 @@
 package retrofit;
 
 import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonWriter;
 import com.squareup.okhttp.MediaType;
 import com.squareup.okhttp.RequestBody;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.Writer;
-import java.lang.reflect.Type;
 import java.nio.charset.Charset;
 import okio.Buffer;
 
@@ -30,19 +31,20 @@ final class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
   private static final Charset UTF_8 = Charset.forName("UTF-8");
 
   private final Gson gson;
-  private final Type type;
+  private final TypeAdapter<T> adapter;
 
-  GsonRequestBodyConverter(Gson gson, Type type) {
+  GsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
     this.gson = gson;
-    this.type = type;
+    this.adapter = adapter;
   }
 
   @Override public RequestBody convert(T value) throws IOException {
     Buffer buffer = new Buffer();
     Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
+    JsonWriter jsonWriter = gson.newJsonWriter(writer);
     try {
-      gson.toJson(value, type, writer);
-      writer.flush();
+      adapter.write(jsonWriter, value);
+      jsonWriter.flush();
     } catch (IOException e) {
       throw new AssertionError(e); // Writing to Buffer does no I/O.
     }
diff --git a/retrofit-converters/gson/src/main/java/retrofit/GsonResponseBodyConverter.java b/retrofit-converters/gson/src/main/java/retrofit/GsonResponseBodyConverter.java
index 0214cfd5ce..5444202e94 100644
--- a/retrofit-converters/gson/src/main/java/retrofit/GsonResponseBodyConverter.java
+++ b/retrofit-converters/gson/src/main/java/retrofit/GsonResponseBodyConverter.java
@@ -15,25 +15,22 @@
  */
 package retrofit;
 
-import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
 import com.squareup.okhttp.ResponseBody;
 import java.io.IOException;
 import java.io.Reader;
-import java.lang.reflect.Type;
 
 final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
-  private final Gson gson;
-  private final Type type;
+  private final TypeAdapter<T> adapter;
 
-  GsonResponseBodyConverter(Gson gson, Type type) {
-    this.gson = gson;
-    this.type = type;
+  GsonResponseBodyConverter(TypeAdapter<T> adapter) {
+    this.adapter = adapter;
   }
 
   @Override public T convert(ResponseBody value) throws IOException {
     Reader reader = value.charStream();
     try {
-      return gson.fromJson(reader, type);
+      return adapter.fromJson(reader);
     } finally {
       Utils.closeQuietly(reader);
     }


Test patch:
diff --git a/retrofit-converters/gson/src/test/java/retrofit/GsonConverterFactoryTest.java b/retrofit-converters/gson/src/test/java/retrofit/GsonConverterFactoryTest.java
index 3351fc5d04..3d4b6e4f0b 100644
--- a/retrofit-converters/gson/src/test/java/retrofit/GsonConverterFactoryTest.java
+++ b/retrofit-converters/gson/src/test/java/retrofit/GsonConverterFactoryTest.java
@@ -121,7 +121,7 @@ interface Service {
   }
 
   @Test public void serializeUsesConfiguration() throws IOException, InterruptedException {
-    server.enqueue(new MockResponse());
+    server.enqueue(new MockResponse().setBody("{}"));
 
     service.anImplementation(new AnImplementation(null)).execute();