123

Is it necessary to wrap in a backing object? I want to do this:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody String str1, @RequestBody String str2) {}

And use a JSON like this:

{
    "str1": "test one",
    "str2": "two test"
}

But instead I have to use:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody Holder holder) {}

And then use this JSON:

{
    "holder": {
        "str1": "test one",
        "str2": "two test"
    }
}

Is that correct? My other option would be to change the RequestMethod to GET and use @RequestParam in query string or use @PathVariable with either RequestMethod.

imTachu
  • 3,491
  • 4
  • 24
  • 52
NimChimpsky
  • 43,542
  • 55
  • 186
  • 295

14 Answers14

100

You are correct, @RequestBody annotated parameter is expected to hold the entire body of the request and bind to one object, so you essentially will have to go with your options.

If you absolutely want your approach, there is a custom implementation that you can do though:

Say this is your json:

{
    "str1": "test one",
    "str2": "two test"
}

and you want to bind it to the two params here:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
public boolean getTest(String str1, String str2)

First define a custom annotation, say @JsonArg, with the JSON path like path to the information that you want:

public boolean getTest(@JsonArg("/str1") String str1, @JsonArg("/str2") String str2)

Now write a Custom HandlerMethodArgumentResolver which uses the JsonPath defined above to resolve the actual argument:

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.jayway.jsonpath.JsonPath;

public class JsonPathArgumentResolver implements HandlerMethodArgumentResolver{

    private static final String JSONBODYATTRIBUTE = "JSON_REQUEST_BODY";
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(JsonArg.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String body = getRequestBody(webRequest);
        String val = JsonPath.read(body, parameter.getMethodAnnotation(JsonArg.class).value());
        return val;
    }

    private String getRequestBody(NativeWebRequest webRequest){
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        String jsonBody = (String) servletRequest.getAttribute(JSONBODYATTRIBUTE);
        if (jsonBody==null){
            try {
                String body = IOUtils.toString(servletRequest.getInputStream());
                servletRequest.setAttribute(JSONBODYATTRIBUTE, body);
                return body;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return "";

    }
}

Now just register this with Spring MVC. A bit involved, but this should work cleanly.

eikooc
  • 1,847
  • 22
  • 31
Biju Kunjummen
  • 45,973
  • 14
  • 107
  • 121
  • 2
    How do I Create custom annotation, say @JsonArg please? – Surendra Jnawali Dec 10 '14 at 07:09
  • Why is this? now I have to create a lot of different wrapper classes in the backend. I am migrating a Struts2 application to Springboot and it had a lot of cases where JSON objects sent using ajax are actually two or more objects of the model: e.g. a User and an Activity – Jose Ospina Nov 11 '16 at 19:06
  • this link show you "how to register this with Spring MVC" http://geekabyte.blogspot.sg/2014/08/how-to-inject-objects-into-spring-mvc.html – Bodil Mar 13 '17 at 16:54
  • 4
    still intersting why this option is not added to spring. it seems like a logical option when you have like 2 longs and do not wont to create a wrapper object for it – tibi Mar 22 '18 at 20:37
  • @SurendraJnawali you can do it like this `@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface JsonArg { String value() default ""; }` – Epono Jun 07 '18 at 09:09
  • So, I can not send multiple request body? I tried 2 requestbody assume int a, int b. But, in curl it is showing "...."Content-Type: application/json" -d "\"3\""". 3 is the body of b – P Satish Patro Apr 15 '19 at 14:17
  • I don't know why, but IOUtils.toString() cannot take any argument for me, did Apache remove this method? However I could use the pattern of this answer to do roughly the same thing. – jdarthenay Oct 03 '19 at 05:42
100

While it's true that @RequestBody must map to a single object, that object can be a Map, so this gets you a good way to what you are attempting to achieve (no need to write a one off backing object):

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody Map<String, String> json) {
   //json.get("str1") == "test one"
}

You can also bind to Jackson's ObjectNode if you want a full JSON tree:

public boolean getTest(@RequestBody ObjectNode json) {
   //json.get("str1").asText() == "test one"
Kong
  • 7,648
  • 14
  • 58
  • 89
  • @JoseOspina why cannot do so. Any risk associated with Map with requestBody – Ben Cheng Jul 10 '18 at 13:52
  • @Ben I mean you can use ONE single `Map` object to store any number of objects inside it, but the top level object must still be only one, there cant be two top level objects. – Jose Ospina Jul 11 '18 at 08:41
  • 1
    I think the downside of a _dynamic_ approach like `Map` is: API documentation libraries (swagger/springfox etc) probably will not be able to parse your request/response schema from your source code. – stratovarius Nov 15 '18 at 09:30
14

For passing multiple object, params, variable and so on. You can do it dynamically using ObjectNode from jackson library as your param. You can do it like this way:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody ObjectNode objectNode) {
   // And then you can call parameters from objectNode
   String strOne = objectNode.get("str1").asText();
   String strTwo = objectNode.get("str2").asText();

   // When you using ObjectNode, you can pas other data such as:
   // instance object, array list, nested object, etc.
}

I hope this help.

azwar_akbar
  • 956
  • 10
  • 25
12

You can mix up the post argument by using body and path variable for simpler data types:

@RequestMapping(value = "new-trade/portfolio/{portfolioId}", method = RequestMethod.POST)
    public ResponseEntity<List<String>> newTrade(@RequestBody Trade trade, @PathVariable long portfolioId) {
...
}
Paul Roub
  • 35,100
  • 27
  • 72
  • 83
shrikeac
  • 121
  • 1
  • 5
2

@RequestParam is the HTTP GET or POST parameter sent by client, request mapping is a segment of URL which's variable:

http:/host/form_edit?param1=val1&param2=val2

var1 & var2 are request params.

http:/host/form/{params}

{params} is a request mapping. you could call your service like : http:/host/form/user or http:/host/form/firm where firm & user are used as Pathvariable.

elmigue017
  • 325
  • 2
  • 12
psisodia
  • 989
  • 4
  • 15
  • 36
2

The easy solution is to create a payload class that has the str1 and the str2 as attributes:

@Getter
@Setter
public class ObjHolder{

String str1;
String str2;

}

And after you can pass

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody ObjHolder Str) {}

and the body of your request is:

{
    "str1": "test one",
    "str2": "two test"
}
Nbenz
  • 402
  • 1
  • 7
  • 12
  • 1
    What is the package of this annotations? Autoimport offered only import jdk.nashorn.internal.objects.annotations.Setter; EDIT. I assume it is Lombok https://projectlombok.org/features/GetterSetter. Please correct me if I am wrong – Gleichmut Mar 26 '20 at 10:23
  • @Gleichmut you can use simple getters and setter for your variables. It will work as you expect. – Gimnath Apr 15 '20 at 07:04
1

Instead of using json, you can do simple thing.

$.post("${pageContext.servletContext.contextPath}/Test",
                {
                "str1": "test one",
                "str2": "two test",

                        <other form data>
                },
                function(j)
                {
                        <j is the string you will return from the controller function.>
                });

Now in the controller you need to map the ajax request as below:

 @RequestMapping(value="/Test", method=RequestMethod.POST)
    @ResponseBody
    public String calculateTestData(@RequestParam("str1") String str1, @RequestParam("str2") String str2, HttpServletRequest request, HttpServletResponse response){
            <perform the task here and return the String result.>

            return "xyz";
}

Hope this helps you.

Japan Trivedi
  • 4,329
  • 2
  • 21
  • 43
1

I have adapted the solution of Biju:

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;


public class JsonPathArgumentResolver implements HandlerMethodArgumentResolver{

    private static final String JSONBODYATTRIBUTE = "JSON_REQUEST_BODY";

    private ObjectMapper om = new ObjectMapper();

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(JsonArg.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String jsonBody = getRequestBody(webRequest);

        JsonNode rootNode = om.readTree(jsonBody);
        JsonNode node = rootNode.path(parameter.getParameterName());    

        return om.readValue(node.toString(), parameter.getParameterType());
    }


    private String getRequestBody(NativeWebRequest webRequest){
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);

        String jsonBody = (String) webRequest.getAttribute(JSONBODYATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
        if (jsonBody==null){
            try {
                jsonBody = IOUtils.toString(servletRequest.getInputStream());
                webRequest.setAttribute(JSONBODYATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return jsonBody;

    }

}

What's the different:

  • I'm using Jackson to convert json
  • I don't need a value in the annotation, you can read the name of the parameter out of the MethodParameter
  • I also read the type of the parameter out of the Methodparameter => so the solution should be generic (i tested it with string and DTOs)

BR

user3227576
  • 484
  • 6
  • 21
1

You can also use a MultiValue Map to hold the requestBody in. here is the example for it.

    foosId -> pathVariable
    user -> extracted from the Map of request Body 

unlike the @RequestBody annotation when using a Map to hold the request body we need to annotate with @RequestParam

and send the user in the Json RequestBody

  @RequestMapping(value = "v1/test/foos/{foosId}", method = RequestMethod.POST, headers = "Accept=application"
            + "/json",
            consumes = MediaType.APPLICATION_JSON_UTF8_VALUE ,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public String postFoos(@PathVariable final Map<String, String> pathParam,
            @RequestParam final MultiValueMap<String, String> requestBody) {
        return "Post some Foos " + pathParam.get("foosId") + " " + requestBody.get("user");
    }
anayagam
  • 81
  • 4
0

request parameter exist for both GET and POST ,For Get it will get appended as query string to URL but for POST it is within Request Body

Kaleem
  • 11
0

Not sure where you add the json but if i do it like this with angular it works without the requestBody: angluar:

    const params: HttpParams = new HttpParams().set('str1','val1').set('str2', ;val2;);
    return this.http.post<any>( this.urlMatch,  params , { observe: 'response' } );

java:

@PostMapping(URL_MATCH)
public ResponseEntity<Void> match(Long str1, Long str2) {
  log.debug("found: {} and {}", str1, str2);
}
tibi
  • 632
  • 1
  • 7
  • 19
0

Good. I suggest creating a Value Object (Vo) that contains the fields you need. The code is simpler, we do not change the functioning of Jackson and it is even easier to understand. Regards!

0

You can achieve what you want by using @RequestParam. For this you should do the following:

  1. Declare the RequestParams parameters that represent your objects and set the required option to false if you want to be able to send a null value.
  2. On the frontend, stringify the objects that you want to send and include them as request parameters.
  3. On the backend turn the JSON strings back into the objects they represent using Jackson ObjectMapper or something like that, and voila!

I know, its a bit of a hack but it works! ;)

Maurice
  • 4,056
  • 5
  • 30
  • 66
0

you can also user @RequestBody Map<String, String> params,then use params.get("key") to get the value of parameter

Will
  • 1
  • 3