Spring MVC how #RequestBody works - java

I thought that #RequestBody tries to map request params to the object after the annotation by the property names.
But if I got:
#RequestMapping(value = "/form", method = RequestMethod.GET)
public #ResponseBody Person formGet(#RequestBody Person p,ModelMap model) {
return p;
}
The request:
http://localhost:8080/proj/home/form?id=2&name=asd
Return 415
When I change #RequestBody Person p with #RequestParam Map<String, String> params it's OK:
#RequestMapping(value = "/form", method = RequestMethod.GET)
public #ResponseBody Person formGet(#RequestParam Map<String, String> params) {
return new Person();
}
Person class:
public class Person{
private long id;
private String name;
public Person() {
}
public Person(long id, String name) {
super();
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Spring vresion 3.2.3.RELEASE
Where did I go wrong?

No, it's a job for #ModelAttribute, not #RequestBody.
#ModelAttribute populates fields of the target object with values of the corresponding request parameters, performing conversions if necessary. It can be used for requests generated by HTML forms, links with parameters, etc.
#RequestBody converts requests to object using one of preconfigured HttpMessageConverters. It can be used for requests containing JSON, XML, etc. However, there is no HttpMessageConverter that replicates behavior of #ModelAttribute.

The conversion of input to a bean needs:
Use POST or PUT request for instance with JSON body.
It is also good to specify expected content tipe by "consumes" in Request mapping:
#RequestMapping(value = "/form", method = RequestMethod.POST, consumes = "application/json" )
Add an instance of converter that implements HttpMessageConverter to the servlet context (servlet.xml for instance)
<bean id="jsonConverter"
class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="supportedMediaTypes" value="application/json" />
</bean>
Add jackson core and mapper jars to classpath (or to pom.xml)
then you can try it using curl
curl -X POST http://localhost:8080/proj/home/form -d '{"name":"asd", "id": 2}' -H 'Content-type:application/json'
Sorry for missing details but I hope that it helps

Related

How to pass not null values #RequestParameter in controller?

I am trying to update an Entity by using spring boot 2.5.3 in the controller method.
http://localhost:5000/api/v1/student/1
with the following payload.
{
"name":"abc",
"email":"abc#email.com",
"dob":"2000-06-14"
}
These values are not updated. They are getting null values when I inspected them using a debugger.
Here is my controller method.
#PutMapping(path = "/{id}")
public ResponseEntity<?> updateStudent(#PathVariable("id") Long id, #RequestParam(required = false) String name, #RequestParam(required = false) String email) {
Student savedStudent = studentService.updateStudent(id, name, email);
return ResponseEntity.ok(savedStudent);
}
Email and name are optional.
In debugger: name:null,email:null. Why are they getting null values?
What is the correct way to pass values from the controller?
#Transactional
// We are not using any query from the repository because we have the service method with transactional annotation.
public Student updateStudent(Long studentId, String name, String email) {
Student student = studentRepository.findById(studentId).orElseThrow(()->new EntityNotFoundException("Student with id " + studentId + " does not exists."));
if (name!= null && name.length()>0 && !Objects.equals(name,student.getName())){
student.setName(name);
}
if (email!= null && email.length()>0 && !Objects.equals(email,student.getEmail())){
Optional<Student> optionalStudent = studentRepository.findStudentByEmail(email);
if (optionalStudent.isPresent()){
throw new IllegalStateException("Email is already taken");
}
student.setEmail(email);
}
System.out.println(student);
Student savedStudent= studentRepository.save(student);
return savedStudent;
}
{
"name":"abc",
"email":"abc#email.com",
"dob":"2000-06-14"
}
This is not a request parameter but the request body. You need to create a class and use #RequestBody annotation.
#Data
public class UpdateStudentRequest {
private String id;
private String name;
private String email;
}
#PutMapping(path = "/{id}")
public ResponseEntity<?> updateStudent(#PathVariable("id") Long id, #RequestBody UpdateStudentRequest request) {
Student savedStudent = studentService.updateStudent(
request.getId(), request.getName(), request.getEmail());
return ResponseEntity.ok(savedStudent);
}
If you want to send the request parameters as... URL parameters:
http://localhost:5000/api/v1/student/1?name=abc&email=abc#email.com
You aren't sending it as a param (after ?).
http://localhost:5000/api/v1/student/1?name=John Could do the trick.
Since you are POSTing an HTTP request with a content body (being in JSON in your case), you need to map the body using the #RequestBody annotation:
#PutMapping(path = "/{id}")
public ResponseEntity<?> updateStudent(#PathVariable("id") Long id, #RequestBody StudentDTO student) {
Student savedStudent = studentService.updateStudent(
id, student.getName(), student.getEmail());
return ResponseEntity.ok(savedStudent);
}
The StudentDTO would be a lightweight type reflecting your input payload:
public class StudentDTO {
private String name;
private String email;
private String dob;
// setters and getters
}
Otherwise, to keep your RestController signature and use the #RequestParametrized fields, you should send a request of following shape:
http://localhost:5000/api/v1/student/1?name=abc&email=abc#email.com&dob=2000-06-14

JsonGetter giving null value

In my Spring Boot application I am creating a REST API, which is calling some other external REST API. I created User class, which is a object that is received by my Rest API downloaded from the external API. My user model looks like:
#JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String fullName;
private String department;
#JsonGetter("fullName")
public String getFullName() {
return fullName;
}
#JsonSetter("full_name")
public void setFullName(String fullName) {
this.fullName = fullName;
}
#JsonGetter("department")
public String getDepartment() {
return department;
}
#JsonSetter("department")
public void setDepartment(String department) {
this.department = department;
}
}
I am using JsonGetter and JsonSetter properties, because I would like to have my json properties in response returned in camelCase, but the properties given in external API are returned with underscore:
External API Response:
{
"full_name": "User A",
"department": "A",
}
My API Response:
{
"fullName": "User A",
"department": "A",
}
And everything seems to be working fine (hitting my API with Postman gives proper responses) until I started to create some Http request tests. In tests I receive assertion error that fullName property is null, while doing the same request in postman is responding with proper responses.
My test class:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {
#LocalServerPort
private int port;
#Autowired
private TestRestTemplate restTemplate;
#Test
public void shouldReturnUserFullName() throws Exception {
assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/users/a",
User.class)).extracting(User::getFullName)
.contains("User A");
}
}
My controller method:
#GetMapping("users/{name}")
public ResponseEntity<User> getSpecificUserByName(#PathVariable("name") String name) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<User> response = restTemplate.exchange(createUriString(name), HttpMethod.GET, entity, User.class);
return response;
}
Test result:
java.lang.AssertionError:
Expecting:
<[null]>
to contain:
<["User A"]>
but could not find:
<["User A"]>
I would appreciate any help with this issue :)
#JsonSetter("full_name") expects your API response to contain a property full_name during deserialzation. Since #JsonGetter("fullName") converts full_name to fullName, field private String fullName; is never set.
You should change #JsonSetter("full_name") to #JsonSetter("fullName").
Let us take an example
Suppose your REST API returns below Object of User class
User reponse = new User();
response.setFullName("User A");
response.setDepartment("A");
So, when we call your REST API, the JSON response would look like as below
{
"fullName":"User A",
"department":"A"
}
Now, When you pass this JSON to convert into User class, Jackson will look for methods with the name setFullName and setDepartment.
In your test case, something similar is happening,
for code
this.restTemplate.getForObject("http://localhost:" + port + "/users/a",User.class)
First, it calls your API to get the User object Serialized and then it Deserialized it to User class. While Deserializing, it looks for a method named
setFullName without any
#Setter
#JsonProperty
#JsonAlias
annotations
or will look for any setter method with
#Setter("fullName")
#JsonProperty("fullName"),
#JsonAlias("fullName")
but in your case, the fullName setter is treated as
public void setFull_name(String fullName) {
this.fullName = fullname;
}
So, setter for fullName is not found but since you marked your User class as
#JsonIgnoreProperties(ignoreUnknown = true)
hence any exception is not thrown but fullName for your Response JSON is ignored, so fullName is never set, which remains null and your Test case is failing.
So, either change your test case or mark your setter with
#JsonAlias("fullName")
annotation.
i.e. Your User class will look like as below
#JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String fullName;
private String department;
#JsonGetter("fullName")
public String getFullName() {
return fullName;
}
#JsonAlias({"fullName","full_name"})
public void setFullName(String fullName) {
this.fullName = fullName;
}
#JsonGetter("department")
public String getDepartment() {
return department;
}
#JsonSetter("department")
public void setDepartment(String department) {
this.department = department;
}
}

How to receive json param in spring boot

I have a Json like the following.
{"person":[{"name":"asd","age":"22"},{"name":"asd","age":"22"}]}
but it could also be:
{"person":[{"name":"asd","age":"22"},{"name":"asd","age":"22"}],"city":["NewYork"],"student":"false"}
How can I receive it in a Spring Boot Controller?
You should use #RequestBody annotation.
#RequestMapping("/api/example")
public String example(#RequestBody String string) {
return string;
}
Later, add some validations and business logic.
You can generate custom class with http://www.jsonschema2pojo.org/. Once generated you can expect your custom class instead of String.
For further instructions, I find this tutorial interesting.
You can receive the json like below, Spring Boot will convert your json into model(For example "Comment" model below) which you defined.
#RequestMapping(value = "/create", method = RequestMethod.POST)
public ResultModel createComment(#RequestBody Comment comment) {...}
1) You need to difine your rest controllers. Example
#Autowired
UserService userService;
#RequestMapping(value = "/user/", method = RequestMethod.GET)
public ResponseEntity<List<User>> listAllUsers() {
List<User> users = userService.findAllUsers();
if (users.isEmpty()) {
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<List<User>>(users, HttpStatus.OK);
}
2) Define your pojo: Example
public class User {
String name;
String age;
public User(String name, String age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public String getAge() {
return age;
}
}
3) Define a service
#Service
public class UserService {
public List<User> findAllUsers(){
// Those are mock data. I suggest to search for Spring-data for interaction with DB.
ArrayList<User> users = new ArrayList<>();
User user = new User("name", "5");
users.add(user);
return users;
}
}
You can follow this tutorial. If you want to just send a json message to a spring boot rest controller you can use a rest client like postman.

Why is Spring Data Neo4J Ignoring #JsonIgnore?

Using the following #NodeEntity
#NodeEntity
public class Person extends BasePersistenceObject {
#GraphId
Long id;
String fullName;
#Indexed(unique=true)
String email;
String passwordHash = null;
#JsonIgnore
public String getPasswordHash() {
return passwordHash;
}
...
}
I'm still seeing the passwordHash in the JSON Response from the following controller method:
#RequestMapping(value = "/login", method=RequestMethod.POST)
public #ResponseBody Person login(#RequestBody Map<String, String> credentials, HttpServletRequest request) throws AuthorizationException {
String email = credentials.get("email");
String password = credentials.get("password");
String ipAddress = request.getRemoteAddr();
return authService.authenticate(email, password, ipAddress);
}
Spring Data Neo4J has nothing to do with JSON serialization, that's the responsibility of Spring MVC and Jackson or Gson, depending on what you use. So make sure, that Jackson is used for JSON serialization, otherwise the annotation wouldn't work.

Parameter binding to a VO with #Form- RestEasy - JAX-Rs

I have a few variables as #PathParam. I want to put them in a Bean and accept all of them in one.
public void show( #PathParam("personId"> String personId,
#PathParam("addressId") String addressId
#Context HttpRequest request) {
// Code
}
Now I would like to put all of the parameters in a Bean/VO with #Form argument.
My class:
class RData {
private String personId;
private String addressId;
private InputStream requestBody;
#PathParam("personId")
public void setPersonId(String personId) {
this.personId = personId;
}
#PathParam("addressId")
public void setAddressId(String addressId) {
this.addressId = addressId;
}
// NOW HERE I NEED TO BIND HttpRequest Context object to request object in my VO.
// That is #Context param in the original method.
}
My method would change to:
public void show( #Form RData rData) {
// Code
}
My VO class above contains what I need to do.
So I need to map #Context HttpRequest request to HttpRequest instance variable in my VO.
How to do that? Because it does not have an attribute name like #PathParam.
You can inject #Context values into properties just like the form, path, and header parameters.
Example Resource Method:
#POST
#Path("/test/{personId}/{addressId}")
public void createUser(#Form MyForm form)
{
System.out.println(form.toString());
}
Example Form Class:
public class MyForm {
private String personId;
private String addressId;
private HttpRequest request;
public MyForm() {
}
#PathParam("personId")
public void setPersonId(String personId) {
this.personId = personId;
}
#PathParam("addressId")
public void setAddressId(String addressId) {
this.addressId = addressId;
}
public HttpRequest getRequest() {
return request;
}
#Context
public void setRequest(HttpRequest request) {
this.request = request;
}
#Override
public String toString() {
return String.format("MyForm: [personId: '%s', addressId: '%s', request: '%s']",
this.personId, this.addressId, this.request);
}
}
Url:
http://localhost:7016/v1/test/1/1
Output:
MyForm: [personId: '1', addressId: '1', request: 'org.jboss.resteasy.plugins.server.servlet.HttpServletInputMessage#15d694da']
I thought I would add an answer for those that are using pure JAX-RS not not RestEasy specifically. Faced with the same problem, and surprised that JAX-RS doesn't have out-of-box support for http Form binding to Java Objects, I created a Java API to marshal/unmarshal java objects to forms, and then used that to create a JAX-RS messagebody reader and writer.
https://github.com/exabrial/form-binding

Categories

Resources