2

I'm trying to send the following java model as form encoded body WITHOUT the wrapping {}. I've tried everything I can find to send a Model NOT as JSON but as form encoded data using Retrofit 2.

// Sends as JSON
@Headers("Content-Type:application/x-www-form-urlencoded")
@POST(SERVICES + USERS)
Observable<UserInfoResponse> signupUser(@Body SignUpParams params);

// Works
@FormUrlEncoded
@POST(SERVICES + USERS)
Observable<UserInfoResponse> signupUser(
        @Field("approve") boolean approve,
        @Field("daily_newsletter") int newsletter,
        @Field("include_order_info") boolean includeOrderInfo,
        @Field("is_21") int is21,
        @Field("is_guest") int isGuest,
        @Field("method") String method,
        @Field("email") String email,
        @Field("password") String password,
        @Field("oauth_token") String oauthToken
);

Here's our setup if it helps

// Dagger Provider
@Provides
@Singleton
@Named(JT_API)
Retrofit provideJTSecureApiRetrofit(OkHttpClient okHttpClient, Gson gson) {
    Retrofit retrofit = new Retrofit.Builder().client(okHttpClient)
            .baseUrl(jtBaseUrl)
            .addConverterFactory(GsonConverterFactory.create(gson))
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build();
    return retrofit;
}

@Provides
@Singleton
OkHttpClient provideOkHttpClient(JTApp app) {
    Interceptor addUrlParams = chain -> {
        Request request = chain.request();
        HttpUrl url = request.url()
            .newBuilder()
            .addQueryParameter("app_version", BuildConfig.VERSION_NAME)
            .build();

        request = request.newBuilder()
            .url(url)
            .build();
        return chain.proceed(request);
    };

    OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();

    okHttpClientBuilder.addInterceptor(addUrlParams);

    // this doesn't seem to do anything…
    okHttpClientBuilder.addInterceptor(chain -> {
        Request original = chain.request();

        Request.Builder requestBuilder = original.newBuilder()
                .addHeader("Content-Type", "application/x-www-form-urlencoded");

        Request request = requestBuilder.build();

        return chain.proceed(request);
    });

    okHttpClientBuilder.readTimeout(JTApp.HTTP_TIMEOUT, TimeUnit.SECONDS)
            .connectTimeout(JTApp.HTTP_TIMEOUT, TimeUnit.SECONDS);

    return okHttpClientBuilder.build();
}

2 Answers2

1

If i am not mistaken

For application/x-www-form-urlencoded, the body of the HTTP message sent to the server is essentially one giant query string -- name/value pairs are separated by the ampersand (&), and names are separated from values by the equals symbol (=).

How to send Form data in retrofit2 android

Community
  • 1
  • 1
skryshtafovych
  • 572
  • 4
  • 16
  • Unfortunately they have the opposite problem from me. Retrofit is sending it as JSON when I want it to be sent as form encoded data. Adding the header does convert it to a giant query string as your definition says it should – Marius Miliunas Oct 20 '16 at 15:17
1

Turns out I had to create my own key value pair converter which extends the Retrofit2 Converter.Factory

/**
 * Retrofit 2 Key Value Pair Form Data Encoder
 *
 * This is a copy over of {@link GsonConverterFactory}. This class sends the outgoing response as
 * form data vs the gson converter which sends it as JSON. The response is proxied through the
 * gson converter factory just the same though
 *
 * Created by marius on 11/17/16.
 */
public class RF2_KeyValuePairConverter extends Converter.Factory {

    private final GsonConverterFactory gsonConverter;

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

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

    private final Gson gson;

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

        this.gsonConverter = GsonConverterFactory.create(gson);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
                                                            Retrofit retrofit) {
        return gsonConverter.responseBodyConverter(type, annotations, retrofit);
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return new KeyValueBodyConverter<>(gson);
    }
}

And here's our KeyValueBody

public class KeyValuePairConverter extends retrofit2.Converter.Factory implements Converter {
    private final Gson gson;

    public KeyValuePairConverter(Gson gson) {
        this.gson = gson;
    }

    // Taken from retrofit's GsonConverter
    @Override
    public Object fromBody(TypedInput body, Type type) throws ConversionException {
        String charset = MimeUtil.parseCharset(body.mimeType());
        InputStreamReader isr = null;
        try {
            isr = new InputStreamReader(body.in(), charset);
            return gson.fromJson(isr, type);
        } catch (IOException e) {
            throw new ConversionException(e);
        } catch (JsonParseException e) {
            throw new ConversionException(e);
        } finally {
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException ignored) {
                }
            }
        }
    }

    @Override
    public TypedOutput toBody(Object object) {
        String json = gson.toJson(object);
        //Log.d( "RETROFIT", json );
        Type type = new TypeToken<Map<String, Object>>() { } .getType();

        // this converts any int values to doubles so we are fixing them back in pojoToTypedOutput
        Map<String, Object> map = gson.fromJson(json, type);
        String body = pojoToTypedOutput(map, null);
        // removes the initial ampersand
        return new TypedString(body.substring(1));
    }

    /**
     * Converts object to list of query parameters
     * (works with nested objects)
     *
     * @todo
     * query parameter encoding
     *
     * @param map           this is the object map
     * @param parentKey     this is the parent key for lists/arrays
     * @return
     */
    public String pojoToTypedOutput(Map<String, Object> map, String parentKey) {
        StringBuffer sb = new StringBuffer();
        if (map != null && map.size() > 0) {
            for (String key : map.keySet()) {
                // recursive call for nested objects
                if (map.get(key).getClass().equals(LinkedTreeMap.class)) {
                    sb.append(pojoToTypedOutput((Map<String, Object>) map.get(key), key));
                } else {
                    // parent key for nested objects
                    Object objectValue = map.get(key);

                    // converts any doubles that really could be ints to integers (0.0 to 0)
                    if (objectValue.getClass().equals(Double.class)) {
                        Double doubleValue = (Double) objectValue;
                        if ((doubleValue == Math.floor(doubleValue)) && !Double.isInfinite(doubleValue)) {
                            objectValue = ((Double) objectValue).intValue();
                        }
                    }

                    if (parentKey != null && parentKey.length() != 0) {
                        sb.append("&").append(key).append("=").append(objectValue);
                    } else {
                        sb.append("&").append(parentKey + "[" + key + "]").append("=").append(objectValue);
                    }
                }
            }
        }
        return sb.toString();
    }
}

In your Retrofit builder add .addConverterFactory(RF2_KeyValuePairConverter.create(gson)) and this will convert your responses to key/value pairs