Dropwizard Multipart Form Quickstart

I hear you are interested in forms with multiple parts

The other day I had the requirement where I had to code an endpoint such that a client could submit two bodies, one containing JSON, and the other one containing XML. One could get clever and embed the XML in JSON, vice versa, or even make two requests, but the HTTP spec already has this issue solved: multipart messages. If you took a peek at the Wikipedia page or the stackoverflow question, “What is [a] HTTP multipart request”, you might be confused because there is a lot of talk about submitting forms, files, and email. These are the typical use cases, but not the only use cases. I’m here to talk about (document) the other use case: multiple bodies.

I’ll be using Dropwizard for the examples but multipart messages should work across frameworks and languages.

Step 1: Add the bundle

First we add the dependency to our pom.xml

<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-forms</artifactId>
</dependency>

And then register the bundle:

@Override
public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
    bootstrap.addBundle(new MultiPartBundle());
}

Step 2: Create resource endpoint

Our resource endpoint will consume JSON content and XML content. Both will be representing the same not-null and valid Person class. We’ll return the difference between the ages of the JSON person and the XML person.

@POST
@Timed
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("/form-quickstart")
public int ageDifference(
    @Valid @NotNull @FormDataParam("json-body") Person jsonPerson,
    @Valid @NotNull @FormDataParam("xml-body") Person xmlPerson
) {
    return jsonPerson.age() - xmlPerson.age();
}

Couple of notes:

  • Notice the usage of pure domain objects. Removing all the annotations would reveal a very simplistic function that has zero ties to REST or HTTP. This allows for a much easier time testing endpoints because, in this case, the business logic can be thoroughly tested like any other domain object
  • There is no notion that the object for the json-body param is even JSON. One could supply JSON for both parts or an entirely different content type that is known.
  • The FormDataParam must come last in the annotation list as Jersey interprets the last annotation specially. Otherwise, you’re going to get those nasty “No injection source found for a parameter”
  • While both Person objects are validated, their validation errors currently lacks the nice context of other *Param annotations. See issue 1782 for more info.

Step 3: Create Test

Assuming you added dropwizard-testing to your pom:

@ClassRule
public static final ResourceTestRule RULE = ResourceTestRule.builder()
        .addResource(new PersonResource())
        .addProvider(MultiPartFeature.class)
        .build();

@Test
public void testResource() {
    final MultiPart multiPartEntity = new FormDataMultiPart()
        .field("json-body", Person.create("Nick", 24), APPLICATION_JSON_TYPE)
        .field("xml-body", Person.create("Papa", 55), APPLICATION_XML_TYPE);

    assertThat(RULE.client().target("/form-quickstart")
        .register(MultiPartFeature.class).request()
        .post(Entity.entity(multiPartEntity, multiPartEntity.getMediaType()))
        .readEntity(String.class))
        .isEqualTo("-31");
}

Here we have to explicitly register the Jersey MultiPartFeature, which is what the bundle did in step 1. It’s a tad inconvenient.

If you have a healthy dose of skepticism whether XML or JSON is really being used you can add another test where you use JSON and XML directly.

final MultiPart multiPartEntity = new FormDataMultiPart()
    .field("json-body", "{ \"age\": 24, \"name\": \"Nick\" }", APPLICATION_JSON_TYPE)
    .field("xml-body", "<Person><age>55</age><name>Papa</name></Person>", APPLICATION_XML_TYPE);

Step 4: Curl Example

Oftentimes across companies and teams, Dropwizard may not be persasive, so handing them the test code snippet as a way to call the API may be inappropriate. That’s why it’s good to include a curl example as an unbiased mechanism for calling the API. It is normally easy enough to work backwards from a curl example to the language or framework of choice. For the API shown earlier a request from curl would look like:

cat > /tmp/tmp-xml-body.xml <<EOF
<root>
  <age>55</age>
  <name>Papa</name>
</root>
EOF

curl 'http://localhost:8080/form-quickstart' \
    -F 'json-body={ "age": 24, "name": "Nick" };type=application/json' \
    -F 'xml-body=</tmp/tmp-xml-body.xml;type=application/xml'

Of course, it came out a little more complicated than I wanted due to my fruitless search in how to escape < in the -F flag. I initially tried embedding the XML data like the JSON data, but curl parses the -F flag and < is a special character signalling to embed the file’s contents.

Specifying type=application/json and type=application/xml is critical as curl interprets the snippets and adds the appropriate Content-Type to each part. This content type is interpreted by Dropwizard, so that it can be deserialized correctly.

This curl statement is translated into the following request:

POST /form-quickstart HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.47.0
Accept: */*
Content-Length: 384
Expect: 100-continue
Content-Type: multipart/form-data; boundary=------------------------9193de6aa3698f01

--------------------------9193de6aa3698f01
Content-Disposition: form-data; name="json-body"
Content-Type: application/json

{ "age": 24, "name": "Nick" }
--------------------------9193de6aa3698f01
Content-Disposition: form-data; name="xml-body"
Content-Type: application/xml

<root>
  <age>55</age>
  <name>Papa</name>
</root>

--------------------------9193de6aa3698f01--

As an aside, I lament the documentation for the -F flag:

lets curl emulate a filled-in form in which a user has pressed the submit button

The documentation makes it appear as if the only use case is someone filling in a form. The sheer amount of documentation missing the use case of having multiple bodies can be misleading. Many times while writing this post I was left scratching my head wondering if what I was doing was against some kind of unspoken law. If it is, feels good.

Step 5: The missing steps

There are some steps that were omitted earlier as they distracted from the main part of using multipart forms:

Since Dropwizard is a JSON first framework, one needs to add in basic XML support:

<dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-xml-provider</artifactId>
    <version>2.7.6</version>
</dependency>

The Jackson package (for Dropwizard 1.0.2) automatically registers an XML provider on the client and server sides, so you don’t have to do anything.

The Person class is described below.

@AutoValue
public abstract class Person {
    @JsonProperty
    public abstract String name();

    @JsonProperty
    public abstract int age();

    @JsonCreator
    public static Person create(
            @JsonProperty("name") String name,
            @JsonProperty("age") int age
    ) {
        return new AutoValue_Person(name, age);
    }
}

I can’t help it, but I really enjoy AutoValue and want its usage to spread

Comments: