1

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);
}
Marquee
  • 1,776
  • 20
  • 21

1 Answers1

1

As per your comment,

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.

When you use @RequestBody you need to create corresponding POJO object to deserialise your JSON. Also you need to add content-type property in your AJAX to indicate the server that you are sending an JSON.

$.ajax({
    method: 'POST',
    data: 'json',
    content-type: 'application/json',
    url: '/commercial-checkout/shipping-stuff',
    data: {
            "shippingAddress": JSON.stringify(shippingAddress),
            "shipDate": shipDate,
            "csrfToken": csrfToken
    }
});

Add a POJO as I mentioned,

public class AddressPOJO {

   shippingAddress,
   shipDate,
   csrfToken  

  //getter / setters
}

Modify your controller method,

@PostMapping(path = "/shipping-stuff", consumes = "application/json")
public RestResponse<?> doShippingStuff( @RequestBody  AddressPOJO addressPJO) {

    // do your logic..
}
ScanQR
  • 3,740
  • 1
  • 13
  • 30
  • Thanks for the response. I did try a version of this, though I didn't include the csrfToken in the wrapper object. I also didn't set the content-type. I'll try it again with it and see if it works. – Marquee May 09 '19 at 06:38