213

Suppose i have a page that lists the objects on a table and i need to put a form to filter the table. The filter is sent as an Ajax GET to an URL like that: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

And instead of having lots of parameters on my Controller like:

@RequestMapping(value = "/action")
public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    @RequestParam(value = "prop1", required = false) String prop1,
    @RequestParam(value = "prop2", required = false) String prop2,
    @RequestParam(value = "prop3", required = false) String prop3) { ... }

And supposing i have MyObject as:

public class MyObject {
    private String prop1;
    private String prop2;
    private String prop3;

    //Getters and setters
    ...
}

I wanna do something like:

@RequestMapping(value = "/action")
public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    @RequestParam(value = "myObject", required = false) MyObject myObject,) { ... }

Is it possible? How can i do that?

renanleandrof
  • 5,822
  • 8
  • 40
  • 57
  • 1
    @michal +1. Here is a couple of tutorials showing how to do that: [Spring 3 MVC: Handling Forms in Spring 3.0 MVC](http://viralpatel.net/blogs/spring-3-mvc-handling-forms/), [What is and how to use `@ModelAttribute`](http://bearprogrammer.wordpress.com/2012/04/11/what-is-and-how-to-use-modelattribute/), [Spring MVC Form Handling Example](http://www.tutorialspoint.com/spring/spring_mvc_form_handling_example.htm). Just google "*Spring MVC form handling*" and you'll get a ton of tutorials/examples. But be sure to use modern way of form handling, i.e. Spring v2.5+ – informatik01 Jun 05 '13 at 15:44
  • Also useful: [What is `@ModelAttribute` in Spring MVC](http://stackoverflow.com/a/3423501/814702) – informatik01 Jun 05 '13 at 15:46

7 Answers7

283

You can absolutely do that, just remove the @RequestParam annotation, Spring will cleanly bind your request parameters to your class instance:

public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    MyObject myObject)
Biju Kunjummen
  • 45,973
  • 14
  • 107
  • 121
  • 38
    Note that Spring by default requiers getters/setters for the MyObject to bind it automatically. Otherway it won't bind the myObject. – aka_sh Dec 31 '14 at 14:29
  • 25
    How can you set fields are optional/non-optional in MyObject? Not sure how to find proper documentation for this.. – worldsayshi Feb 25 '15 at 21:54
  • Works perfectly. Do you have a link to the documentation, specifically if I can combine it with @Valid? – Jan Dec 15 '15 at 09:11
  • 7
    @Biju, Is there a way to control default values and required for `MyObject` then, in a similar way we can do with @RequestParam? – Alexey Jan 23 '17 at 10:38
  • 1
    Yes @Alexey, you can do this via `@ModelAttribute` - if you have a `@ModelAttribute` annotated method you can provide the default values there and then your real method can add in other details from the request - here is a link with more information on how to do this: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#mvc-ann-modelattrib-methods – Biju Kunjummen Jan 26 '17 at 01:05
  • this solutions works with objects, but dont work with Lists. I'm trying to get a List of MyObject. Any idea? – David Canós Jun 29 '17 at 09:17
  • 10
    @BijuKunjummen How can I update the `MyObject` to accept query parameters in Snake case and map it to a camel case member of `MyObject`. For example, `?book_id=4`, should be mapped with `bookId` member of the `MyObject`? – Vivek Vardhan Aug 16 '17 at 12:17
  • @worldsayshi I suppose, since Java 8, you could use Optional fields for this. – RudyVerboven Oct 06 '17 at 12:49
  • If there is also an `@RequestParam` in the method parameters then the only way I could get this to work was by adding `@ModelAttribute`, if no `@RequestParam` are present, then using a plain object without annotation also worked. – lanoxx Mar 19 '18 at 16:10
  • @worldsayshi If I got you right, you can validate body with `@Valid` & standard validation annotations – Frankie Drake Sep 19 '18 at 11:01
  • 2
    I have request params as such: `page-number` and `page-size`. How will my request DTO object look like? In java, I can't create a field with the name `page-number` or `page-size`. Is there a way to tell spring that the request param with the given name should be mapped to this or that field? Please advise. – Ihor M. Jun 05 '19 at 21:34
  • To add on [aka_sh(]https://stackoverflow.com/users/271146/aka-sh) comment. You can still make it work with Jackson Annotation, or Mixins. `class MyObject { public final String prop1; @JsonCreator MyObject(@JsonProperty("prop1") String prop1){} }`. Then you don't need gettter/settters. – fan Apr 12 '21 at 13:15
59

I will add some short example from me.

The DTO class:

public class SearchDTO {
    private Long id[];

    public Long[] getId() {
        return id;
    }

    public void setId(Long[] id) {
        this.id = id;
    }
    // reflection toString from apache commons
    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

Request mapping inside controller class:

@RequestMapping(value="/handle", method=RequestMethod.GET)
@ResponseBody
public String handleRequest(SearchDTO search) {
    LOG.info("criteria: {}", search);
    return "OK";
}

Query:

http://localhost:8080/app/handle?id=353,234

Result:

[http-apr-8080-exec-7] INFO  c.g.g.r.f.w.ExampleController.handleRequest:59 - criteria: SearchDTO[id={353,234}]

I hope it helps :)

UPDATE / KOTLIN

Because currently I'm working a lot of with Kotlin if someone wants to define similar DTO the class in Kotlin should have the following form:

class SearchDTO {
    var id: Array<Long>? = arrayOf()

    override fun toString(): String {
        // to string implementation
    }
}

With the data class like this one:

data class SearchDTO(var id: Array<Long> = arrayOf())

the Spring (tested in Boot) returns the following error for request mentioned in answer:

"Failed to convert value of type 'java.lang.String[]' to required type 'java.lang.Long[]'; nested exception is java.lang.NumberFormatException: For input string: \"353,234\""

The data class will work only for the following request params form:

http://localhost:8080/handle?id=353&id=234

Be aware of this!

Przemek Nowak
  • 5,289
  • 3
  • 43
  • 51
  • 2
    is it possible to set "required" to dto fields? – Normal May 27 '16 at 14:06
  • 4
    I suggest to try with Spring MVC validators. Example: http://www.codejava.net/frameworks/spring/spring-mvc-form-validation-example-with-bean-validation-api – Przemek Nowak May 27 '16 at 15:53
  • It's very curious that this does not require an annotation! I wonder, is there an explicit annotation for this albeit unnecessary? – James Watkins Aug 31 '18 at 18:10
  • This behavior difference with java/kotlin should be an error. I created an issue. Spend few hours to search why it was not working but I used to have no problem in java. See https://github.com/spring-projects/spring-framework/issues/25815 – Yoann CAPLAIN Sep 25 '20 at 09:32
  • Thanks for Kotlin way, but there is any way to immutable object and use val instead var to work ? – Mohsen Jan 05 '21 at 15:59
8

Since the question on how to set fields mandatory pops up under each post, I wrote a small example on how to set fields as required:

public class ExampleDTO {
    @NotNull
    private String mandatoryParam;

    private String optionalParam;
    
    @DateTimeFormat(iso = ISO.DATE) //accept Dates only in YYYY-MM-DD
    @NotNull
    private LocalDate testDate;

    public String getMandatoryParam() {
        return mandatoryParam;
    }
    public void setMandatoryParam(String mandatoryParam) {
        this.mandatoryParam = mandatoryParam;
    }
    public String getOptionalParam() {
        return optionalParam;
    }
    public void setOptionalParam(String optionalParam) {
        this.optionalParam = optionalParam;
    }
    public LocalDate getTestDate() {
        return testDate;
    }
    public void setTestDate(LocalDate testDate) {
        this.testDate = testDate;
    }
}

//Add this to your rest controller class
@RequestMapping(value = "/test", method = RequestMethod.GET)
public String testComplexObject (@Valid ExampleDTO e){
    System.out.println(e.getMandatoryParam() + " " + e.getTestDate());
    return "Does this work?";
}
  • Doest that work today? Tried here, but the validations that I set on the pojo didnt work. – Wally Dec 17 '20 at 20:55
  • 1
    Yes it still works. Maybe this question will help you https://stackoverflow.com/questions/43436804/spring-valid-doesnt-work If you need more assistance I need some example code to look at it further. – FluffyDestroyerOfCode Dec 20 '20 at 09:33
7

I have a very similar problem. Actually the problem is deeper as I thought. I am using jquery $.post which uses Content-Type:application/x-www-form-urlencoded; charset=UTF-8 as default. Unfortunately I based my system on that and when I needed a complex object as a @RequestParam I couldn't just make it happen.

In my case I am trying to send user preferences with something like;

 $.post("/updatePreferences",  
    {id: 'pr', preferences: p}, 
    function (response) {
 ...

On client side the actual raw data sent to the server is;

...
id=pr&preferences%5BuserId%5D=1005012365&preferences%5Baudio%5D=false&preferences%5Btooltip%5D=true&preferences%5Blanguage%5D=en
...

parsed as;

id:pr
preferences[userId]:1005012365
preferences[audio]:false
preferences[tooltip]:true
preferences[language]:en

and the server side is;

@RequestMapping(value = "/updatePreferences")
public
@ResponseBody
Object updatePreferences(@RequestParam("id") String id, @RequestParam("preferences") UserPreferences preferences) {

    ...
        return someService.call(preferences);
    ...
}

I tried @ModelAttribute, added setter/getters, constructors with all possibilities to UserPreferences but no chance as it recognized the sent data as 5 parameters but in fact the mapped method has only 2 parameters. I also tried Biju's solution however what happens is that, spring creates an UserPreferences object with default constructor and doesn't fill in the data.

I solved the problem by sending JSon string of the preferences from the client side and handle it as if it is a String on the server side;

client:

 $.post("/updatePreferences",  
    {id: 'pr', preferences: JSON.stringify(p)}, 
    function (response) {
 ...

server:

@RequestMapping(value = "/updatePreferences")
public
@ResponseBody
Object updatePreferences(@RequestParam("id") String id, @RequestParam("preferences") String preferencesJSon) {


        String ret = null;
        ObjectMapper mapper = new ObjectMapper();
        try {
            UserPreferences userPreferences = mapper.readValue(preferencesJSon, UserPreferences.class);
            return someService.call(userPreferences);
        } catch (IOException e) {
            e.printStackTrace();
        }
}

to brief, I did the conversion manually inside the REST method. In my opinion the reason why spring doesn't recognize the sent data is the content-type.

hevi
  • 1,842
  • 1
  • 26
  • 41
  • 5
    I am having exactly the same issue too. I did cleaner workaround using `@RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)` – Petr Újezdský Dec 14 '16 at 20:30
0

While answers that refer to @ModelAttribute, @RequestParam, @PathParam and the likes are valid, there is a small gotcha I ran into. The resulting method parameter is a proxy that Spring wraps around your DTO. So, if you attempt to use it in a context that requires your own custom type, you may get some unexpected results.

The following will not work:

@GetMapping(produces = APPLICATION_JSON_VALUE)
public ResponseEntity<CustomDto> request(@ModelAttribute CustomDto dto) {
    return ResponseEntity.ok(dto);
}

In my case, attempting to use it in Jackson binding resulted in a com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

You will need to create a new object from the dto.

DKO
  • 43
  • 7
0

Yes, You can do it in a simple way. See below code of lines.

URL - http://localhost:8080/get/request/multiple/param/by/map?name='abc' & id='123'

 @GetMapping(path = "/get/request/header/by/map")
    public ResponseEntity<String> getRequestParamInMap(@RequestParam Map<String,String> map){
        // Do your business here 
        return new ResponseEntity<String>(map.toString(),HttpStatus.OK);
    } 
vkstream
  • 508
  • 5
  • 7
0

Accepted answer works like a charm but if the object has a list of objects it won't work as expected so here is my solution after some digging.

Following this thread advice, here is how I've done.

  • Frontend: stringify your object than encode it in base64 for submission.
  • Backend: decode base64 string then convert the string json into desired object.

It isn't the best for debugging your API with postman but it is working as expected for me.

Original object: { page: 1, size: 5, filters: [{ field: "id", value: 1, comparison: "EQ" }

Encoded object: eyJwYWdlIjoxLCJzaXplIjo1LCJmaWx0ZXJzIjpbeyJmaWVsZCI6ImlkUGFyZW50IiwiY29tcGFyaXNvbiI6Ik5VTEwifV19

@GetMapping
fun list(@RequestParam search: String?): ResponseEntity<ListDTO> {
    val filter: SearchFilterDTO = decodeSearchFieldDTO(search)
    ...
}

private fun decodeSearchFieldDTO(search: String?): SearchFilterDTO {
    if (search.isNullOrEmpty()) return SearchFilterDTO()
    return Gson().fromJson(String(Base64.getDecoder().decode(search)), SearchFilterDTO::class.java)
}

And here the SearchFilterDTO and FilterDTO

class SearchFilterDTO(
    var currentPage: Int = 1,
    var pageSize: Int = 10,
    var sort: Sort? = null,
    var column: String? = null,
    var filters: List<FilterDTO> = ArrayList<FilterDTO>(),
    var paged: Boolean = true
)

class FilterDTO(
    var field: String,
    var value: Any,
    var comparison: Comparison
)
Gabriel Brito
  • 532
  • 1
  • 7
  • 19