JSON parameter in spring MVC controller
Asked Answered
D

5

35

I have

@RequestMapping(method = RequestMethod.GET)
@ResponseBody
SessionInfo register(UserProfile profileJson){
  ...
}

I pass profileJson this way:

http://server/url?profileJson={"email": "[email protected]"}

but my profileJson object has all null fields. What should I do to make spring parse my json?

Dialogize answered 5/2, 2014 at 12:49 Comment(7)
Passing JSON into the query parameter doesn't make sense. You need to at least URL-encode it.Hunker
You are passing json as a URL parameter not as the body (which is the default). Passing JSON as a parameter in general doesn't make sense. Annotate your method argument with @RequestParam. However as mentioned you should be passing it as the body of the request and probably also as a POST instead of a GET request.Tweeter
you almost certainly want to use POST here, having request bodies for a get is highly uncommon (https://mcmap.net/q/40417/-http-get-with-request-body)Hydric
I use jsonp, it doesn't support POST. Annotating parameter with @RequestParam gives exception 'no matching editors or conversion strategy found'Dialogize
Just get the parameter as a String and convert it yourself.Poock
Dear Sotirios, this is how I do it currently, but after code review I was asked to do it elegantly.Dialogize
(When replying to someone, use @their-name. Otherwise, they won't get a notification.) Alternatively, you can write your HttpMessageConverter, but it doesn't really make any sense to use @RequestBody if the content you want is not in the body.Poock
K
27

This could be done with a custom editor, that converts the JSON into a UserProfile object:

public class UserProfileEditor extends PropertyEditorSupport  {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        ObjectMapper mapper = new ObjectMapper();

        UserProfile value = null;

        try {
            value = new UserProfile();
            JsonNode root = mapper.readTree(text);
            value.setEmail(root.path("email").asText());
        } catch (IOException e) {
            // handle error
        }

        setValue(value);
    }
}

This is for registering the editor in the controller class:

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(UserProfile.class, new UserProfileEditor());
}

And this is how to use the editor, to unmarshall the JSONP parameter:

@RequestMapping(value = "/jsonp", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
SessionInfo register(@RequestParam("profileJson") UserProfile profileJson){
  ...
}
Kellerman answered 10/2, 2014 at 22:18 Comment(3)
This looks so generic, I wonder if it's really necessary to write and register a custom editor for every class I want to be parsed that way.Boogiewoogie
Adding this to the dispatcher servlet xml worked for me '<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" /> </mvc:message-converters> </mvc:annotation-driven>'Nobe
It can be even simpler with a Converter. See my answer for an example.Shuffleboard
H
29

The solution to this is so easy and simple it will practically make you laugh, but before I even get to it, let me first emphasize that no self-respecting Java developer would ever, and I mean EVER work with JSON without utilizing the Jackson high-performance JSON library.

Jackson is not only a work horse and a defacto JSON library for Java developers, but it also provides a whole suite of API calls that makes JSON integration with Java a piece of cake (you can download Jackson at http://jackson.codehaus.org/).

Now for the answer. Assuming that you have a UserProfile pojo that looks something like this:

public class UserProfile {

private String email;
// etc...

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

// more getters and setters...
}

...then your Spring MVC method to convert a GET parameter name "profileJson" with JSON value of {"email": "[email protected]"} would look like this in your controller:

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper; // this is your lifesaver right here

//.. your controller class, blah blah blah

@RequestMapping(value="/register", method = RequestMethod.GET) 
public SessionInfo register(@RequestParam("profileJson") String profileJson) 
throws JsonMappingException, JsonParseException, IOException {

    // now simply convert your JSON string into your UserProfile POJO 
    // using Jackson's ObjectMapper.readValue() method, whose first 
    // parameter your JSON parameter as String, and the second 
    // parameter is the POJO class.

    UserProfile profile = 
            new ObjectMapper().readValue(profileJson, UserProfile.class);

        System.out.println(profile.getEmail());

        // rest of your code goes here.
}

Bam! You're done. I would encourage you to look through the bulk of Jackson API because, as I said, it is a lifesaver. For example, are you returning JSON from your controller at all? If so, all you need to do is include JSON in your lib, and return your POJO and Jackson will AUTOMATICALLY convert it into JSON. You can't get much easier than that. Cheers! :-)

Holomorphic answered 12/2, 2014 at 3:23 Comment(7)
I wish here was some parameter for the @RequestParameter annotation for this.Hoax
It's not good practice to create for every request ObjectMapper. Controller should have field ObjectMapper.Pinckney
Actually, that's incorrect. First of all, I'm assuming by "field" you mean an instance variable of the controller class. Secondly, the point of my example was to demonstrate the use of ObjectMapper, not give best practices and delve into finer architecture details of object instantiation. And thirdly, and most importantly, having ObjectMapper as an instance variable of a controller is just flat-out bad practice because Spring controllers are singletons by default. The correct approach would be follow standard MVC patterns and Autowire a service class which contains an ObjectMapper.Holomorphic
@TheSaint I'm just being curious, why it's bad practice for singleton controller to have a field like this?Nisan
@grape_mao, the ObjectMapper is not considered to be fully thread-safe in Jackson 2.x, according to its author: https://mcmap.net/q/80033/-should-i-declare-jackson-39-s-objectmapper-as-a-static-fieldByran
It would be more elegant (and probably more efficient) to create a Converter and let Spring MVC use it automatically. See my answer for an example.Shuffleboard
@theSaint In which dependency do I find SessionInfo?Hest
K
27

This could be done with a custom editor, that converts the JSON into a UserProfile object:

public class UserProfileEditor extends PropertyEditorSupport  {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        ObjectMapper mapper = new ObjectMapper();

        UserProfile value = null;

        try {
            value = new UserProfile();
            JsonNode root = mapper.readTree(text);
            value.setEmail(root.path("email").asText());
        } catch (IOException e) {
            // handle error
        }

        setValue(value);
    }
}

This is for registering the editor in the controller class:

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(UserProfile.class, new UserProfileEditor());
}

And this is how to use the editor, to unmarshall the JSONP parameter:

@RequestMapping(value = "/jsonp", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseBody
SessionInfo register(@RequestParam("profileJson") UserProfile profileJson){
  ...
}
Kellerman answered 10/2, 2014 at 22:18 Comment(3)
This looks so generic, I wonder if it's really necessary to write and register a custom editor for every class I want to be parsed that way.Boogiewoogie
Adding this to the dispatcher servlet xml worked for me '<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" /> </mvc:message-converters> </mvc:annotation-driven>'Nobe
It can be even simpler with a Converter. See my answer for an example.Shuffleboard
S
6

You can create your own Converter and let Spring use it automatically where appropriate:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
class JsonToUserProfileConverter implements Converter<String, UserProfile> {

    private final ObjectMapper jsonMapper = new ObjectMapper();

    public UserProfile convert(String source) {
        return jsonMapper.readValue(source, UserProfile.class);
    }
}

As you can see in the following controller method nothing special is needed:

@GetMapping
@ResponseBody
public SessionInfo register(@RequestParam UserProfile userProfile)  {
  ...
}

Spring picks up the converter automatically if you're using component scanning and annotate the converter class with @Component.

Learn more about Spring Converter and type conversions in Spring MVC.

Shuffleboard answered 15/5, 2018 at 12:38 Comment(3)
Given that this looks quite generic, can we just parametrize the class? class JsonToUserProfileConverter<T> implements Converter<String, T> Will Spring still automatically pick this class up and use it (by inferring T from the result type)?Nardoo
Spring resolves type parameters, so that should work as expected (only exception are nested type parameters with Kotlin types, what not always works).Shuffleboard
It worked for me after "manually" adding the converter by overriding addFormatters() in WebMvcConfigurerFreak
Y
2

This does solve my immediate issue, but I'm still curious as to how you might pass in multiple JSON objects via an AJAX call.

The best way to do this is to have a wrapper object that contains the two (or multiple) objects you want to pass. You then construct your JSON object as an array of the two objects i.e.

[
  {
    "name" : "object1",
    "prop1" : "foo",
    "prop2" : "bar"
  },
  {
    "name" : "object2",
    "prop1" : "hello",
    "prop2" : "world"
  }
]

Then in your controller method you recieve the request body as a single object and extract the two contained objects. i.e:

@RequestMapping(value="/handlePost", method = RequestMethod.POST, consumes = {      "application/json" })
public void doPost(@RequestBody WrapperObject wrapperObj) { 
     Object obj1 = wrapperObj.getObj1;
     Object obj2 = wrapperObj.getObj2;

     //Do what you want with the objects...


}

The wrapper object would look something like...

public class WrapperObject {    
private Object obj1;
private Object obj2;

public Object getObj1() {
    return obj1;
}
public void setObj1(Object obj1) {
    this.obj1 = obj1;
}
public Object getObj2() {
    return obj2;
}
public void setObj2(Object obj2) {
    this.obj2 = obj2;
}   

}
Yore answered 22/6, 2016 at 4:59 Comment(0)
Y
-6

Just add @RequestBody annotation before this param

Yongyoni answered 5/2, 2014 at 13:15 Comment(2)
This doesn't work. I get HTTP 415: The server refused this request because the request entity is in a format not supported by the requested resource for the requested method.Dialogize
@RequestBody by default looks at the body of the request. OP's content is a request parameter sent as a part of the query string.Poock

© 2022 - 2024 — McMap. All rights reserved.