Repository: square/retrofit
Issue: Allow to serialize nulls in moshi request body converter
Labels: Enhancement

Hi!

Seems like there is an issue in Moshi converter. Would be nice to be able control writters serializeNulls property when using Moshi converter.

Sincerely,
Roman


Issue comments:
Yep. Since Moshi doesn't have any top-level configuration around this we'll provide options on the converter.


Pull request review comments:

Patch that resolved the issue:
diff --git a/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiConverterFactory.java b/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiConverterFactory.java
index d566c3044c..60e5f1bfd8 100644
--- a/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiConverterFactory.java
+++ b/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiConverterFactory.java
@@ -50,21 +50,28 @@ public static MoshiConverterFactory create() {
 
   /** Create an instance using {@code moshi} for conversion. */
   public static MoshiConverterFactory create(Moshi moshi) {
-    return new MoshiConverterFactory(moshi, false);
+    if (moshi == null) throw new NullPointerException("moshi == null");
+    return new MoshiConverterFactory(moshi, false, false);
   }
 
   private final Moshi moshi;
   private final boolean lenient;
+  private final boolean serializeNulls;
 
-  private MoshiConverterFactory(Moshi moshi, boolean lenient) {
-    if (moshi == null) throw new NullPointerException("moshi == null");
+  private MoshiConverterFactory(Moshi moshi, boolean lenient, boolean serializeNulls) {
     this.moshi = moshi;
     this.lenient = lenient;
+    this.serializeNulls = serializeNulls;
   }
 
   /** Return a new factory which uses {@linkplain JsonAdapter#lenient() lenient} adapters. */
   public MoshiConverterFactory asLenient() {
-    return new MoshiConverterFactory(moshi, true);
+    return new MoshiConverterFactory(moshi, true, serializeNulls);
+  }
+
+  /** Return a new factory which includes null values into the serialized JSON. */
+  public MoshiConverterFactory withNullSerialization() {
+    return new MoshiConverterFactory(moshi, lenient, true);
   }
 
   @Override
@@ -84,7 +91,7 @@ public Converter<?, RequestBody> requestBodyConverter(Type type,
     if (lenient) {
       adapter = adapter.lenient();
     }
-    return new MoshiRequestBodyConverter<>(adapter);
+    return new MoshiRequestBodyConverter<>(adapter, serializeNulls);
   }
 
   private static Set<? extends Annotation> jsonAnnotations(Annotation[] annotations) {
diff --git a/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiRequestBodyConverter.java b/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiRequestBodyConverter.java
index ac3d095d35..5e59b17fed 100644
--- a/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiRequestBodyConverter.java
+++ b/retrofit-converters/moshi/src/main/java/retrofit2/converter/moshi/MoshiRequestBodyConverter.java
@@ -16,6 +16,7 @@
 package retrofit2.converter.moshi;
 
 import com.squareup.moshi.JsonAdapter;
+import com.squareup.moshi.JsonWriter;
 import java.io.IOException;
 import okhttp3.MediaType;
 import okhttp3.RequestBody;
@@ -26,14 +27,18 @@ final class MoshiRequestBodyConverter<T> implements Converter<T, RequestBody> {
   private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
 
   private final JsonAdapter<T> adapter;
+  private final boolean serializeNulls;
 
-  MoshiRequestBodyConverter(JsonAdapter<T> adapter) {
+  MoshiRequestBodyConverter(JsonAdapter<T> adapter, boolean serializeNulls) {
     this.adapter = adapter;
+    this.serializeNulls = serializeNulls;
   }
 
   @Override public RequestBody convert(T value) throws IOException {
     Buffer buffer = new Buffer();
-    adapter.toJson(buffer, value);
+    JsonWriter writer = JsonWriter.of(buffer);
+    writer.setSerializeNulls(serializeNulls);
+    adapter.toJson(writer, value);
     return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
   }
 }


Test patch:
diff --git a/retrofit-converters/moshi/src/test/java/retrofit2/converter/moshi/MoshiConverterFactoryTest.java b/retrofit-converters/moshi/src/test/java/retrofit2/converter/moshi/MoshiConverterFactoryTest.java
index 3b902d1d8c..d22c372678 100644
--- a/retrofit-converters/moshi/src/test/java/retrofit2/converter/moshi/MoshiConverterFactoryTest.java
+++ b/retrofit-converters/moshi/src/test/java/retrofit2/converter/moshi/MoshiConverterFactoryTest.java
@@ -119,6 +119,7 @@ interface Service {
 
   private Service service;
   private Service serviceLenient;
+  private Service serviceNulls;
 
   @Before public void setUp() {
     Moshi moshi = new Moshi.Builder()
@@ -137,6 +138,7 @@ interface Service {
         .build();
     MoshiConverterFactory factory = MoshiConverterFactory.create(moshi);
     MoshiConverterFactory factoryLenient = factory.asLenient();
+    MoshiConverterFactory factoryNulls = factory.withNullSerialization();
     Retrofit retrofit = new Retrofit.Builder()
         .baseUrl(server.url("/"))
         .addConverterFactory(factory)
@@ -145,8 +147,13 @@ interface Service {
         .baseUrl(server.url("/"))
         .addConverterFactory(factoryLenient)
         .build();
+    Retrofit retrofitNulls = new Retrofit.Builder()
+        .baseUrl(server.url("/"))
+        .addConverterFactory(factoryNulls)
+        .build();
     service = retrofit.create(Service.class);
     serviceLenient = retrofitLenient.create(Service.class);
+    serviceNulls = retrofitNulls.create(Service.class);
   }
 
   @Test public void anInterface() throws IOException, InterruptedException {
@@ -207,6 +214,14 @@ interface Service {
     assertThat(body.theName).isEqualTo("value");
   }
 
+  @Test public void withNulls() throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setBody("{}"));
+
+    Call<AnImplementation> call = serviceNulls.anImplementation(new AnImplementation(null));
+    call.execute();
+    assertEquals("{\"theName\":null}", server.takeRequest().getBody().readUtf8());
+  }
+
   @Test public void utf8BomSkipped() throws IOException {
     Buffer responseBody = new Buffer()
         .write(ByteString.decodeHex("EFBBBF"))