JAX-RS Post multiple objects

84,893

Solution 1

The answer is no.

The reason is simple: This about the parameters you can receive in a method. They must be related to the request. Right? So they must be either headers or cookies or query parameters or matrix parameters or path parameters or request body. (Just to tell the complete story there is additional types of parameters called context).

Now, when you receive JSON object in your request, you receive it in a request body. How many bodies the request may have? One and only one. So you can receive only one JSON object.

Solution 2

You can not use your method like this as correctly stated by Tarlog.

However, you can do this:

@POST
@Path("test")
@Consumes(MediaType.APPLICATION_JSON)
public void test(List<ObjectOne> objects)

or this:

@POST
@Path("test")
@Consumes(MediaType.APPLICATION_JSON)
public void test(BeanWithObjectOneAndObjectTwo containerObject)

Furthermore, you can always combine your method with GET parameters:

@POST
@Path("test")
@Consumes(MediaType.APPLICATION_JSON)
public void test(List<ObjectOne> objects, @QueryParam("objectTwoId") long objectTwoId)

Solution 3

If we look at what the OP is trying to do, he/she is trying to post two (possibly unrelated) JSON objects. First any solution to try and send one part as the body, and one part as some other param, IMO, are horrible solutions. POST data should go in the body. It's not right to do something just because it works. Some work-arounds might be violating basic REST principles.

I see a few solutions

  1. Use application/x-www-form-urlencoded
  2. Use Multipart
  3. Just wrap them in a single parent object

1. Use application/x-www-form-urlencoded

Another option is to just use application/x-www-form-urlencoded. We can actually have JSON values. For examle

curl -v http://localhost:8080/api/model \
     -d 'one={"modelOne":"helloone"}' \
     -d 'two={"modelTwo":"hellotwo"}'

public class ModelOne {
    public String modelOne;
}

public class ModelTwo {
    public String modelTwo;
}

@Path("model")
public class ModelResource {

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public String post(@FormParam("one") ModelOne modelOne,
                       @FormParam("two") ModelTwo modelTwo) {
        return modelOne.modelOne + ":" + modelTwo.modelTwo;
    }
}

The one thing we need to get this to work is a ParamConverterProvider to get this to work. Below is one that has been implemented by Michal Gadjos of the Jersey Team (found here with explanation).

@Provider
public class JacksonJsonParamConverterProvider implements ParamConverterProvider {

    @Context
    private Providers providers;

    @Override
    public <T> ParamConverter<T> getConverter(final Class<T> rawType,
                                              final Type genericType,
                                              final Annotation[] annotations) {
        // Check whether we can convert the given type with Jackson.
        final MessageBodyReader<T> mbr = providers.getMessageBodyReader(rawType,
                genericType, annotations, MediaType.APPLICATION_JSON_TYPE);
        if (mbr == null
              || !mbr.isReadable(rawType, genericType, annotations, MediaType.APPLICATION_JSON_TYPE)) {
            return null;
        }

        // Obtain custom ObjectMapper for special handling.
        final ContextResolver<ObjectMapper> contextResolver = providers
                .getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE);

        final ObjectMapper mapper = contextResolver != null ?
                contextResolver.getContext(rawType) : new ObjectMapper();

        // Create ParamConverter.
        return new ParamConverter<T>() {

            @Override
            public T fromString(final String value) {
                try {
                    return mapper.reader(rawType).readValue(value);
                } catch (IOException e) {
                    throw new ProcessingException(e);
                }
            }

            @Override
            public String toString(final T value) {
                try {
                    return mapper.writer().writeValueAsString(value);
                } catch (JsonProcessingException e) {
                    throw new ProcessingException(e);
                }
            }
        };
    }
}

If you aren't scanning for resource and providers, just register this provider, and the above example should work.

2. Use Multipart

One solution that no one has mentioned, is to use multipart. This allows us to send arbitrary parts in a request. Since each request can only have one entity body, multipart is the work around, as it allows to have different parts (with their own content types) as part of the entity body.

Here is an example using Jersey (see official doc here)

Dependency

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-multipart</artifactId>
    <version>${jersey-2.x.version}</version>
</dependency>

Register the MultipartFeature

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/api")
public class JerseyApplication extends ResourceConfig {

    public JerseyApplication() {
        packages("stackoverflow.jersey");
        register(MultiPartFeature.class);
    }
}

Resource class

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.media.multipart.FormDataParam;

@Path("foobar")
public class MultipartResource {

    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response postFooBar(@FormDataParam("foo") Foo foo,
                               @FormDataParam("bar") Bar bar) {
        String response = foo.foo + "; " + bar.bar;
        return Response.ok(response).build();
    }

    public static class Foo {
        public String foo;
    }

    public static class Bar {
        public String bar;
    }
}

Now the tricky part with some clients is that there isn't a way to set the Content-Type of each body part, which is required for the above to work. The multipart provider will look up message body reader, based on the type of each part. If it's not set to application/json or a type, the Foo or Bar has a reader for, this will fail. We will use JSON here. There's no extra configuration but to have a reader available. I'll use Jackson. With the below dependency, no other configuration should be required, as the provider will be discovered through classpath scanning.

<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>${jersey-2.x.version}</version>
</dependency>

Now the test. I will be using cURL. You can see I explicitly set the Content-Type for each part with type. The -F signifies to different part. (See very bottom of the post for an idea of how the request body actually looks.)

curl -v -X POST \ -H "Content-Type:multipart/form-data" \ -F "bar={\"bar\":\"BarBar\"};type=application/json" \ -F "foo={\"foo\":\"FooFoo\"};type=application/json" \ http://localhost:8080/api/foobar
Result: FooFoo; BarBar

The result is exactly as we expected. If you look at the resource method, all we do is return this string foo.foo + "; " + bar.bar, gathered from the two JSON objects.

You can see some examples using some different JAX-RS clients, in the links below. You will also see some server side example also from those different JAX-RS implementations. Each link should have somewhere in it a link to the official documentation for that implementation

There are other JAX-RS implementations out there, but you will need to find the documentation for it yourself. The above three are the only ones I have experience with.

As far as Javascript clients, most of the example I see (e.g. some of these involve setting the Content-Type to undefined/false (using FormData), letting the Browser handle the it. But this will not work for us, as the Browser will not set the Content-Type for each part. And the default type is text/plain.

I'm sure there are libraries out there that allow setting the type for each part, but just to show you how it can be done manually, I'll post an example (got a little help from here. I'll be using Angular, but the grunt work of building the entity body will be plain old Javascript.

<!DOCTYPE html>
<html ng-app="multipartApp">
    <head>
        <script src="js/libs/angular.js/angular.js"></script>
        <script>
            angular.module("multipartApp", [])
            .controller("defaultCtrl", function($scope, $http) {

                $scope.sendData = function() {
                    var foo = JSON.stringify({foo: "FooFoo"});
                    var bar = JSON.stringify({bar: "BarBar"});

                    var boundary = Math.random().toString().substr(2);                    
                    var header = "multipart/form-data; charset=utf-8; boundary=" + boundary;

                    $http({
                        url: "/api/foobar",
                        headers: { "Content-Type": header }, 
                        data: createRequest(foo, bar, boundary),
                        method: "POST"
                    }).then(function(response) {
                        $scope.result = response.data;
                    });
                };

                function createRequest(foo, bar, boundary) {
                    var multipart = "";
                    multipart += "--" + boundary
                        + "\r\nContent-Disposition: form-data; name=foo"
                        + "\r\nContent-type: application/json"
                        + "\r\n\r\n" + foo + "\r\n";        
                    multipart += "--" + boundary
                        + "\r\nContent-Disposition: form-data; name=bar"
                        + "\r\nContent-type: application/json"
                        + "\r\n\r\n" + bar + "\r\n";
                    multipart += "--" + boundary + "--\r\n";
                    return multipart;
                }
            });
        </script>
    </head>
    <body>
        <div ng-controller="defaultCtrl">
            <button ng-click="sendData()">Send</button>
            <p>{{result}}</p>
        </div>
    </body>
</html>

The interesting part is the createRequest function. This is where we build the multipart, setting the Content-Type of each part to application/json, and concatenating the stringified foo and bar objects to each part. If you are unfamiliar multipart format see here for more info. The other interesting part is the header. We set it to multipart/form-data.

Below is the result. In Angular I just used the result to show in the HTML, with $scope.result = response.data. The button you see was just to make the request. You will also see the request data in firebug

enter image description here

3. Just wrap them in a single parent object

This option should be self explanatory, as others have already mentioned.

Solution 4

The next approach is usually applied in this kind of cases:

TransferObject {
    ObjectOne objectOne;
    ObjectTwo objectTwo;

    //getters/setters
}

@POST
@Path("test")
@Consumes(MediaType.APPLICATION_JSON)
public void test(TransferObject object){
//        object.getObejctOne()....
}

Solution 5

You can't put two separate objects in one single POST call as explained by Tarlog.

Anyway you could create a third container object that contains the first two objects and pass that one within the POS call.

Share:
84,893

Related videos on Youtube

Thizzer
Author by

Thizzer

Software developer at heart. Interested in all things Tech.

Updated on July 05, 2022

Comments

  • Thizzer
    Thizzer over 1 year

    I have a method;

    @POST
    @Path("test")
    @Consumes(MediaType.APPLICATION_JSON)
    public void test(ObjectOne objectOne, ObjectTwo objectTwo)
    

    Now I know I can post a single object in json format, just putting it into the body. But is it possible to do multiple objects? If so, how?

  • Tarlog
    Tarlog over 10 years
    @Scholle I wonder how did you decide that my answer is about subsequent requests?
  • Sanjay Kumar
    Sanjay Kumar about 10 years
    Last one (with QueryParam) ain't working. Error - "The server refused this request because the request entity is in a format not supported by the requested resource for the requested method (Unsupported Media Type)"
  • Stijn de Witt
    Stijn de Witt about 7 years
    Mmmm a List<SomeEntity> has multiple objects, no?
  • Tarlog
    Tarlog about 7 years
    @StijndeWitt Array can contain multiple objects. This is the purpose of array, right? JAX-RS API cannot receive multiple objects. So as tine2k states in his answer: you can have container object that will contain multiple objects.
  • JustStartedProgramming
    JustStartedProgramming about 6 years
    @tine2k for me List<Object> objects is not working here I post my problem link
  • Skippy le Grand Gourou
    Skippy le Grand Gourou almost 4 years
    To Java newcomers : don’t google ObjectOne, it’s not a special object class but a dummy name you have to replace by your own class name.
  • Balazs Kelemen
    Balazs Kelemen about 3 years
    The answer "no" is FALSE. "How many bodies the request may have?" In a "multipart/form-data", there can be more bodies.
  • Balazs Kelemen
    Balazs Kelemen about 3 years
    This answer is great, however the question was JAX-RS, and the multipart-solution provided is in Jersey. It took me some time to convert it to JAX-RS, that seams to be even more simple.