Passing multiple variables in @RequestBody to a Spring MVC controller using Ajax

JavaSpringHttpSpring Mvc

Java Problem Overview


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.

Java Solutions


Solution 1 - Java

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"

Solution 2 - Java

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.

Solution 3 - Java

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.

Solution 4 - Java

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) {
...
}

Solution 5 - Java

@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.

Solution 6 - Java

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"
}

Solution 7 - Java

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.

Solution 8 - Java

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

Solution 9 - Java

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");
    }

Solution 10 - Java

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

Solution 11 - Java

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);
}

Solution 12 - Java

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!

Solution 13 - Java

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! ;)

Solution 14 - Java

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

Solution 15 - Java

Use an inner class

@RestController
public class MyController {

    @PutMapping("/do-thing")
    public void updateFindings(@RequestBody Bodies.DoThing body) {
        ...
    }


    private static class Bodies {
        public static class DoThing {
            public String name;
            public List<String> listOfThings;
        }
    }
}

Solution 16 - Java

If somebody is interested in the webflux solution, below is a reactive version, based on Biju answer.

Please note that there is one very small but synchronized chunk, needed to protect the body from being consumed more than once. If you prefer a fully non-blocking version, I suggest publishing the flux that obtains json on the same scheduler, to make checking and reading sequential.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Slf4j
@RequiredArgsConstructor
public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
    private static final String ATTRIBUTE_KEY = "BODY_TOSTRING_RESOLVER";
    private final ObjectMapper objectMapper;


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


    @Override
    public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
                                        ServerWebExchange exchange) {
        String fieldName = parameter.getParameterName();
        Class<?> clz = parameter.getParameterType();

        return getRequestBody(exchange).map(body -> {
            try {
                JsonNode jsonNode = objectMapper.readTree(body).get(fieldName);
                String s = jsonNode.toString();
                return objectMapper.readValue(s, clz);
            } catch (JsonProcessingException e) {
                log.error(e.getMessage(), e);
                throw new RuntimeException(e);
            }
        });
    }


    private Mono<String> getRequestBody(ServerWebExchange exchange) {
        Mono<String> bodyReceiver;
        synchronized (exchange) {
            bodyReceiver = exchange.getAttribute(ATTRIBUTE_KEY);
            if (bodyReceiver == null) {
                bodyReceiver = exchange.getRequest().getBody()
                        .map(this::convertToString)
                        .single()
                        .cache();
                exchange.getAttributes().put(ATTRIBUTE_KEY, bodyReceiver);
            }
        }
        return bodyReceiver;
    }

    private String convertToString(DataBuffer dataBuffer) {
        byte[] bytes = new byte[dataBuffer.readableByteCount()];
        dataBuffer.read(bytes);
        DataBufferUtils.release(dataBuffer);
        return new String(bytes, StandardCharsets.UTF_8);
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNimChimpskyView Question on Stackoverflow
Solution 1 - JavaKongView Answer on Stackoverflow
Solution 2 - JavaBiju KunjummenView Answer on Stackoverflow
Solution 3 - Javaazwar_akbarView Answer on Stackoverflow
Solution 4 - JavashrikeacView Answer on Stackoverflow
Solution 5 - JavapsisodiaView Answer on Stackoverflow
Solution 6 - JavaNbenzView Answer on Stackoverflow
Solution 7 - JavaJapan TrivediView Answer on Stackoverflow
Solution 8 - Javauser3227576View Answer on Stackoverflow
Solution 9 - JavaanayagamView Answer on Stackoverflow
Solution 10 - JavaKaleemView Answer on Stackoverflow
Solution 11 - JavatibiView Answer on Stackoverflow
Solution 12 - JavaMatias ZamoranoView Answer on Stackoverflow
Solution 13 - JavaMauriceView Answer on Stackoverflow
Solution 14 - JavaBrookView Answer on Stackoverflow
Solution 15 - JavacambunctiousView Answer on Stackoverflow
Solution 16 - JavavingaView Answer on Stackoverflow