Repository: square/retrofit
Issue: @Field.encodeName/Value support

Hi there!
What do you think about implementing `encodeName` and `encodeName` for the `@Field` annotation, similar to you recent commit for `@Query` (https://github.com/square/retrofit/commit/46598adb1da0f6d2498d9f8b815b170bf95fe1f8)? It will allow not to URL-encode specific fields (at the moment they are being encoded unconditionally).
I need this for my project, and I'd be happy to implement this, would it be okay? 
The actual reason I need something like that this is because `URLEncoder.encode` converts spaces to "+" signs, whereas I need to convert them to "%20" (I can't alter the decoding behavior on the server side), and there seems no way of intercepting encoding in Retrofit, since `URLEncoder` usage is hardcoded.
A more generic solution would be a dependency injection, like: `new RestAdapter.Builder().setURLEncoder(myURLEncoder)`. However, I am not sure if a lot of people suffer of a similar problem and really need this, so I think I will be fine with annotating that one field with `@Field(encodeValue = false)`.


Issue comments:
4c38147577d47227b05c0bdb0e8bfefd0f995dec


Pull request review comments:
Isn't this a change in behavior? At the very least, not matching it to `@Field` is strange.

Oops, that was accidental. Fixed. 

This is a very unfortunate public API that we have to expose. Can we at least pair up the `boolean` params with their `String` counterpart.

`true` is the default, no need to specify

don't specify the `true` defualt


Patch that resolved the issue:
diff --git a/retrofit/src/main/java/retrofit/RequestBuilder.java b/retrofit/src/main/java/retrofit/RequestBuilder.java
index 913581f35f..be6fe6404e 100644
--- a/retrofit/src/main/java/retrofit/RequestBuilder.java
+++ b/retrofit/src/main/java/retrofit/RequestBuilder.java
@@ -272,26 +272,32 @@ void setArguments(Object[] args) {
         }
       } else if (annotationType == Field.class) {
         if (value != null) { // Skip null values.
-          String name = ((Field) annotation).value();
+          Field field = (Field) annotation;
+          String name = field.value();
+          boolean encodeName = field.encodeName();
+          boolean encodeValue = field.encodeValue();
           if (value instanceof Iterable) {
             for (Object iterableValue : (Iterable<?>) value) {
               if (iterableValue != null) { // Skip null values.
-                formBody.addField(name, iterableValue.toString());
+                formBody.addField(name, iterableValue.toString(), encodeName, encodeValue);
               }
             }
           } else if (value.getClass().isArray()) {
             for (int x = 0, arrayLength = Array.getLength(value); x < arrayLength; x++) {
               Object arrayValue = Array.get(value, x);
               if (arrayValue != null) { // Skip null values.
-                formBody.addField(name, arrayValue.toString());
+                formBody.addField(name, arrayValue.toString(), encodeName, encodeValue);
               }
             }
           } else {
-            formBody.addField(name, value.toString());
+            formBody.addField(name, value.toString(), encodeName, encodeValue);
           }
         }
       } else if (annotationType == FieldMap.class) {
         if (value != null) { // Skip null values.
+          FieldMap fieldMap = (FieldMap) annotation;
+          boolean encodeNames = fieldMap.encodeNames();
+          boolean encodeValues = fieldMap.encodeValues();
           for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
             Object entryKey = entry.getKey();
             if (entryKey == null) {
@@ -300,7 +306,8 @@ void setArguments(Object[] args) {
             }
             Object entryValue = entry.getValue();
             if (entryValue != null) { // Skip null values.
-              formBody.addField(entryKey.toString(), entryValue.toString());
+              formBody.addField(entryKey.toString(), entryValue.toString(),
+                  encodeNames, encodeValues);
             }
           }
         }
diff --git a/retrofit/src/main/java/retrofit/http/Field.java b/retrofit/src/main/java/retrofit/http/Field.java
index cdceca1a67..fce09d9c0e 100644
--- a/retrofit/src/main/java/retrofit/http/Field.java
+++ b/retrofit/src/main/java/retrofit/http/Field.java
@@ -56,4 +56,10 @@
 @Retention(RUNTIME)
 public @interface Field {
   String value();
+
+  /** Specifies whether {@link #value()} is URL encoded. */
+  boolean encodeName() default true;
+
+  /** Specifies whether the argument value to the annotated method parameter is URL encoded. */
+  boolean encodeValue() default true;
 }
diff --git a/retrofit/src/main/java/retrofit/http/FieldMap.java b/retrofit/src/main/java/retrofit/http/FieldMap.java
index df1b925e89..09b6f4c0de 100644
--- a/retrofit/src/main/java/retrofit/http/FieldMap.java
+++ b/retrofit/src/main/java/retrofit/http/FieldMap.java
@@ -44,4 +44,9 @@
 @Target(PARAMETER)
 @Retention(RUNTIME)
 public @interface FieldMap {
+  /** Specifies whether parameter names (keys in the map) are URL encoded. */
+  boolean encodeNames() default true;
+
+  /** Specifies whether parameter values (values in the map) are URL encoded. */
+  boolean encodeValues() default true;
 }
diff --git a/retrofit/src/main/java/retrofit/mime/FormUrlEncodedTypedOutput.java b/retrofit/src/main/java/retrofit/mime/FormUrlEncodedTypedOutput.java
index d807195a29..73c8f70667 100644
--- a/retrofit/src/main/java/retrofit/mime/FormUrlEncodedTypedOutput.java
+++ b/retrofit/src/main/java/retrofit/mime/FormUrlEncodedTypedOutput.java
@@ -24,6 +24,10 @@ public final class FormUrlEncodedTypedOutput implements TypedOutput {
   final ByteArrayOutputStream content = new ByteArrayOutputStream();
 
   public void addField(String name, String value) {
+    addField(name, value, true, true);
+  }
+
+  public void addField(String name, String value, boolean encodeName, boolean encodeValue) {
     if (name == null) {
       throw new NullPointerException("name");
     }
@@ -34,8 +38,12 @@ public void addField(String name, String value) {
       content.write('&');
     }
     try {
-      name = URLEncoder.encode(name, "UTF-8");
-      value = URLEncoder.encode(value, "UTF-8");
+      if (encodeName) {
+        name = URLEncoder.encode(name, "UTF-8");
+      }
+      if (encodeValue) {
+        value = URLEncoder.encode(value, "UTF-8");
+      }
 
       content.write(name.getBytes("UTF-8"));
       content.write('=');


Test patch:
diff --git a/retrofit/src/test/java/retrofit/RequestBuilderTest.java b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
index 5c2af1caf7..9a331f358c 100644
--- a/retrofit/src/test/java/retrofit/RequestBuilderTest.java
+++ b/retrofit/src/test/java/retrofit/RequestBuilderTest.java
@@ -1456,6 +1456,30 @@ Response method(@Field("foo") String foo, @Field("ping") String ping) {
     assertTypedBytes(request.getBody(), "foo=bar&ping=pong");
   }
 
+  @Test public void formEncodedWithEncodedNameFieldParam() {
+    class Example {
+      @FormUrlEncoded //
+      @POST("/foo") //
+      Response method(@Field(value = "na+me", encodeName = false, encodeValue = true) String foo) {
+        return null;
+      }
+    }
+    Request request = buildRequest(Example.class, "ba r");
+    assertTypedBytes(request.getBody(), "na+me=ba+r");
+  }
+
+  @Test public void formEncodedWithEncodedValueFieldParam() {
+    class Example {
+      @FormUrlEncoded //
+      @POST("/foo") //
+      Response method(@Field(value = "na me", encodeName = true, encodeValue = false) String foo) {
+        return null;
+      }
+    }
+    Request request = buildRequest(Example.class, "ba+r");
+    assertTypedBytes(request.getBody(), "na+me=ba+r");
+  }
+
   @Test public void formEncodedFieldOptional() {
     class Example {
       @FormUrlEncoded //
@@ -1511,6 +1535,40 @@ Response method(@Field("foo") int[] fields, @Field("kit") String kit) {
     assertTypedBytes(request.getBody(), "foo=1&foo=2&foo=3&kit=kat");
   }
 
+  @Test public void formEncodedWithEncodedNameFieldParamMap() {
+    class Example {
+      @FormUrlEncoded //
+      @POST("/foo") //
+      Response method(@FieldMap(encodeNames = false, encodeValues = true) Map<String, Object> fieldMap) {
+        return null;
+      }
+    }
+
+    Map<String, Object> fieldMap = new LinkedHashMap<String, Object>();
+    fieldMap.put("k+it", "k at");
+    fieldMap.put("pin+g", "po ng");
+
+    Request request = buildRequest(Example.class, fieldMap);
+    assertTypedBytes(request.getBody(), "k+it=k+at&pin+g=po+ng");
+  }
+
+  @Test public void formEncodedWithEncodedValueFieldParamMap() {
+    class Example {
+      @FormUrlEncoded //
+      @POST("/foo") //
+      Response method(@FieldMap(encodeNames = true, encodeValues = false) Map<String, Object> fieldMap) {
+        return null;
+      }
+    }
+
+    Map<String, Object> fieldMap = new LinkedHashMap<String, Object>();
+    fieldMap.put("k it", "k+at");
+    fieldMap.put("pin g", "po+ng");
+
+    Request request = buildRequest(Example.class, fieldMap);
+    assertTypedBytes(request.getBody(), "k+it=k+at&pin+g=po+ng");
+  }
+
   @Test public void formEncodedFieldMap() {
     class Example {
       @FormUrlEncoded //