Why would a JSON be rendered in bracket notation when bound to a Web Request?
I have an application that makes 2 REST Controller calls in a row.
First, an address is read from a form, serialized, AJAXed to a validation endpoint, and returned to the UI/JS as a Response object payload. This part works fine.
$.ajax({
method: 'POST',
url: '/validate-shipping-address',
data: $shippingInfoForm.serialize()
}
@PostMapping(path = "/validate-shipping-address")
public RestResponse<Address> validateShippingAddress(
@ModelAttribute("shippingForm") ShippingForm shippingForm) {
return validateAddress(shippingForm.getAddress());
}
If the response is successful, that payload (the address) is sent directly into the AJAX call for the second endpoint. This request blows up with a 400
and never enters the actual method.
$.ajax({
method: 'POST',
url: '/shipping-stuff',
data: {
"shippingAddress": validationResponse.payload,
"shipDate": shipDate,
"csrfToken": csrfToken
}
}
@PostMapping(path = "/shipping-stuff")
public RestResponse<?> doShippingStuff(
@RequestParam(name = "shippingAddress") Address shippingAddress,
@RequestParam(name = "shipDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date shippingDate) {
doStuff(); // Never hit
}
After much analysis, the issue is that Spring MVC cannot deserialize the address passed as an Address object. In the request, I see that the address fields are rendered as address[city]
, address[state]
, etc. instead of the standard dot notation address.city
, address.state
(as the first request has it). If I manually access them via the request using the bracket notation as the param name, it will pull out the value. e.g. request.getParameter("address[city]");
.
When I use Chrome dev tools debugger to inspect the response from the first and the object entering the second AJAX call, they look like valid JSON. The Network:Form Data section in Chrome differs though - It shows the dot notation for the first, successful call and the bracket notation for the second, unsuccessful call.
Form Data (first call):
address.firstName=John&address.lastName=Doe&address.addressLine1=123+Main+St&address.city=New+York+City&address.state=NY&address.postalCode=12345&csrfToken=XXXX
Form Data (second call): (%5B = '[' and %5D = ']')
shippingAddress%5BfirstName%5D=John&shippingAddress%5BlastName%5D=Doe&shippingAddress%5BaddressLine1%5D=123+MAIN+ST&shippingAddress%5Bcity%5D=New+York+City&shippingAddress%5Bstate%5D=NY&shippingAddress%5BpostalCode%5D=12345&shippingAddress%5BzipFour%5D=6789&shipDate=2019-05-25&csrfToken=XXXX
So there really are 2 sub-questions:
(1) Is there something in the JS handling that would cause the address to be passed in the bracket form instead of the normal JSON form? I'd think if I could get around this then Spring MVC should work like normal.
(2) Is there a way to make Spring MVC able to handle this without resorting to JSON.stringify
and then parsing with Gson/ObjectMapper directly in the controller code?
I've tried all permutations I can think of involving custom wrapper objects, JSON.stringify
, @RequestParam
, @ModelAttribute
, and bare (no annotations). I also tried stringify-ing the whole AJAX payload and using @RequestBody
on a wrapper object in the controller, but then the call fails as the csrfToken is not detected.
I've read through this, this, and this, which informed the attempts above.
For now, I've worked around the issue with JSON.stringify and Gson (option 2 above), but would rather make Spring MVC do the work automatically.
Work around:
$.ajax({
method: 'POST',
url: '/commercial-checkout/shipping-stuff',
data: {
"shippingAddress": JSON.stringify(shippingAddress),
"shipDate": shipDate,
"csrfToken": csrfToken
}
});
@PostMapping(path = "/shipping-stuff")
public RestResponse<?> doShippingStuff( //
@RequestParam(name = "shippingAddress") String shippingAddressJson,
@RequestParam(name = "shipDate") @DateTimeFormat(pattern = "yyyy-MM-dd") Date shipDate) {
Address address = gson.fromJson(shippingAddressJson, AddressImpl.class);
}