Retrofit: Passing Array of Objects as FormUrlEncoded Fields

Created on 22 May 2014  Â·  19Comments  Â·  Source: square/retrofit

I need to talk to an API that accepts an array of images in the following format:

images[0][url]=https://www.filepicker.io/api/file/abc&images[0][height]=425&images[1][url]=https://www.filepicker.io/api/file/12345&images[1][height]=425

Is there a simple way to do this with Retrofit? Array field output seems to be different with my testing so far.

Most helpful comment

You could also use @FieldMap and build up all the items yourself.

All 19 comments

Array output simply duplicates the form field name for each value. Form URL encoding has crazy uses all over which are questionably compliant to the spec. Most would be impossible to implement in a general way so we will not be adding any kind of first-party support for this.

However, you can do this yourself. The easiest way is a custom Converter that can do serialization in the manner you require.

public class Image {
  public final String url;
  public final int height;

  public Image(String url, int height) {
    this.url = url;
    this.height = height;
  }
}
public class ImagesConverter implements Converter {
  @Override public TypedOutput toBody(Object object) {
    if (!object.getClass().isArray()) {
      // TODO Maybe implement this?
      throw new UnsupportedOperationException();
    }

    try {
      FormUrlEncodedTypedOutput form = new FormUrlEncodedTypedOutput();
      for (int i = 0, size = Array.getLength(object); i < size; i++) {
        Object item = Array.get(object, i);
        for (Field field : item.getClass().getFields()) {
          String value = String.valueOf(field.get(item));
          form.addField("images[" + i + "][" + field.getName() + "]", value);
        }
      }
      return form;
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  @Override public Object fromBody(TypedInput body, Type type) throws ConversionException {
    throw new UnsupportedOperationException();
  }
}
public interface MyService {
  @POST("/images")
  Response void sendImages(@Body List<Image> images);
}
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("...")
    .setConverter(new MyConverter())
    .build();
MyService myService = restAdapter.create(MyService.class);

myService.sendImages(Arrays.asList(new Image("foo", 20), new Image("bar", 50)));

You could also use @FieldMap and build up all the items yourself.

How would one build up the items to pass to a @FieldMap argument? It seems to require the argument is of type Map<String,String> when we need something like Map<String, List<Map<String,String>>> (a mouthful I know). Is this not possible until #626 is completed?

This is the form of the data we're trying to send

{
   otherProperty: 1,
   answers: [{questionId: 1, value: "..."},{questionId: 2, value: "..."},{questionId: 3, value: "..."},...]
}

We essentially need a to pass something of type List<Map<String,String>> which I'd settle for since I can't seem to figure out how to pass List<Answer> using a strongly typed argument.

The converter seems like a possible option but I'd have to write it much more generically unless we created separate services just for a couple of endpoints. We'd also have to consider the response and this feels like something I'd love to avoid.

I do have power to change or add an endpoint but would like to know how to bend Retrofit to conform to this type of form payload as it appears commonly in Rails APIs.

Thanks a lot of any suggestions.

That data is still just a Map<String, String> where the second key's value is a complex JSON object.

Hey thanks for responding.

I think I get what you're saying but not exactly following.

Would I build the map up in JSON format like this:

m.add("answers", "[{questionId: 1, value: "..."},...");

or do I need to use this type of format (or something similar):

m.add("answers[0]", "questionId=1value=foo");
m.add("answers[1]", "questionId=2value=bar");

I'm not really sure of the semantics of how you would create a complex nested object like that. It would have to be form-encoded data inside form-encoded data which isn't something you see often (I've never seen it in use). Usually form-encoded data is only used for simply key/value pairs and for something complex you would use something with structure like JSON instead.

We definitely do not offer any easy way to create nested form-encoding like that.

It's incredibly common in rails. It's the format accepts_nested_attributes_for uses if you or anyone is at all familiar.

But no worries. You've given me enough to work with. I will post back my solution for anyone else who might stumble upon this issue.

Thanks for the help and an awesome library.

Sent from my iPhone

On Aug 6, 2015, at 3:12 PM, Jake Wharton [email protected] wrote:

I'm not really sure of the semantics of how you would create a complex nested object like that. It would have to be form-encoded data inside form-encoded data which isn't something you see often (I've never seen it in use). Usually form-encoded data is only used for simply key/value pairs and for something complex you would use something with structure like JSON instead.

We definitely do not offer any easy way to create nested form-encoding like that.

—
Reply to this email directly or view it on GitHub.

At the end of the day I ended up with something that looks like this:

 @POST("/enrollments/{id}/intake_questions")
    Observable<Response> submitOnboardingAnswers(@Path("id") int enrollmentId,
                                                 @Field("answers[][question_id]") Integer question_id,
                                                 @Field("answers[][value]") String value,
                                                 @Field("answers[][selection_index") String selection_index);

This let me post one at a time. The API Gem that we're using grape let's you declare array params but it's actually kind of flimsy how it processes them. It expects params to appear like this.

http://.../intake_questions?
answers[][question_id]=1
&answers[][value]=an+answer
&answers[][question_id]=25
&answers[][value]=the+real+deal

I couldn't manage to get Retrofit to generate ordered params. I'm not too sure that it should. I didn't want to go nuts with a custom converter so I just zipped up n iterables and posted n times.

Just a paper trail for anyone else. Again thx for retrofit. Looking forward to 2.0!

And I learned that Grape will actually accept a post body so I was able to get it to work by removing @FormUrlEncoded and using @Body instead of @Field.

@POST("/notes")
Observable<ResponseNote> postNote(@Body Note note);

///Test


@Test
public void testHotness() {
  Note note = new Note();
  note.setEnrollmentId(106);
  note.setEvent("duderino");
  Answer answer = new Answer();
  answer.setQuestionId(10);
  answer.setValue("Such a great answer");
  note.getAnswers().add(answer);
  Answer b = new Answer();
  b.setQuestionId(7);
  b.setValue("amazing");
  note.getAnswers().add(b);

  ResponseNote response = api.postNote(note).toBlocking().first();
  Assertions.assertThat(response.getNote()).isNotNull();
  ....
}

Again, just a paper trail. Nothing for you to see Jake. But this really simplified my original code and got me exactly what I wanted.

Hello JakeWharton!
You may repeatedly answered this question. But I could not find the correct information on this subject. I hope you can help. The problem has already been mentioned. No problem with request like:

@FormUrlEncoded
@POST("/guide/confirm")
Call<Model> confirm(@Field("step") String step,  @Field("code") String code);

But, what is correct (!) way do encode all object (which has three or more list).

@// What need to do, to encode all data below?
@POST("/guide/loadinfo")
Call<Model> confirm(@Body VeryBigJsonObject object);

You can use @FieldMap

Happy holidays to tou!
As I can say, this is simple way using with fixed fields. So using @FieldMap, I cant send requst with many fields and other list (list of other fields).

Some example, what I need to send - http://www.jsoneditoronline.org/?id=661b2bae9eb520902825a58f8d44c338

That's JSON, not a url-encoded form.

On Thu, Dec 31, 2015 at 8:08 AM GensaGames [email protected] wrote:

Happy holidays to tou!
As I can say, this is simple way using with fixed fields. So using
@FieldMap, I cant send requst with many fields and other list (list of
other fields).

Some example, what I need to send -
http://www.jsoneditoronline.org/?id=661b2bae9eb520902825a58f8d44c338

—
Reply to this email directly or view it on GitHub
https://github.com/square/retrofit/issues/490#issuecomment-168191776.

Excuse me again. I have one part of projects that great works with retrofit. But I have a few requests that just does not simple. And retrofit docs has not enough information about. For example, can i send request, like (below), or how it make another way:

@FormUrlEncoded
    @POST("/api/tour/save")
    Call<LoginModel> loadTourData(@FieldMap Map<String, String> mainItems,
                                  @Field("images")List<HashMap<String, String>> images);

@GensaGames I believe you can use Gson (https://github.com/google/gson) and do the following to the params that you pass into your FieldMap (assuming images is a List<> and your param map is called params).

params.put("images", new Gson().toJson(images));

This should allow you to remove the extra Field.

I have a gist here which has parity with AFNetworking's approach on iOS. I haven't been bit using this yet.

To post array object as form-url_encoded just use this in retrofit.

@POST(ApiConstants.JOB_CARD_SAVE)
@FormUrlEncoded
Call<MasterResponse<JobCardSaveResponse>> jobCardSave(@FieldMap Map<String, Object> 
_jobCardRequest,@Field("qty[]")
 List<String> qty,@Field("service_id[]") List<String> service_id);

Just put your array object in @field.

In case if someone looking for this.

If your request match something like the one below, the following code works fine.
[{"key1: "value1"", "key2":"value2", "arraykey":["v1","v2"]}]

In case if someone looking for this.
Your can just pass your complex object and it will take of converting to json. Works well.

@Headers(JwtKeyTokenWithValue)
@POST("accounts/{accountId}/assets")
fun createXXXX(@Path("accountId") accountId: String, @Body list: List): Single

Hello.
How do I send Array, String>???

Was this page helpful?
0 / 5 - 0 ratings