I need to use groups so that I can use the same DTO for creating and patching; however, the requirements for these operations are obviously different. Therefore, I decided to use Spirng's #Validated instead of #Valid along with groups. Here is the simplified version of my code:
class PersonDto{
#NotBlank(groups = CreateConstraint.class)
private String phoneNumber;
#NotNull(groups = CreateConstraint.class)
#Valid
private AddressDto address;
}
and in the AddressDto, I have used the default group:
#Getter
#Setter
public class AddressDto {
#NotBlank
private String line1;
private String line2;
private String city;
#NotNull
private Province province;
#NotBlank
private String postalCode;
}
And the controller:
#PatchMapping("/patch")
public void patchPerson(#RequestBody #Validated PersonDto dto) {
personService.patchPerson(dto);}
In the controller here I am using the default group as well. Now, unless I explicitly add #Valid to the AddressDto declaration in PersonDto, the validation is not done for the AddressDto. why is it that #Validated is not cascaded to the fields inside the PersonDto? Thanks.
Edit:
Just to be clear #Valid does validate recursively. wondering why #Validated not...
You have to add the validation group to the cascaded fields as well.
For example, check the line1 field for the CreateConstraint group.
Also, you have to specify the validation group in the controller.
#Getter
#Setter
public class AddressDto {
#NotBlank(groups = CreateConstraint.class)
private String line1;
private String line2;
private String city;
#NotNull()
private Province province;
#NotBlank()
private String postalCode;
}
#PatchMapping("/patch")
public void patchPerson(#RequestBody #Validated({CreateConstraint.class})PersonDto dto) {
personService.patchPerson(dto);
}
Related
So I have been playing around with Spring building a full stack application and I am at the point where I am validating data. First off, I want to validate that the incoming post request to my Spring Controller is able to create a User object and add it to the database. I created a custom validator using the combinator pattern and Java 8 features to do this.
Question 1 is there a way to intercept one of the JSON fields in post method? It is coming back as a String but I need a localdate to satisfy the user object creation/validation.
Question 2 when is it preferred to use validation annotations in the POJO object vs validating when the request comes through the controller? Should you be using both? Are there preferred patterns for validation? Obviosuly the client side will be validated as well before the info reaches the server.
//Using my custom validation here
#PostMapping
public ResponseEntity addUser(#RequestBody User user) {
UserValidation.ValidationResult result = userValidationService.validate(user);
if (result.equals(UserValidation.ValidationResult.SUCCESS)){
logger.info("Added a new user.");
userService.addUser(user);
return ResponseEntity.ok(HttpStatus.OK);
}
logger.info("Could not add new user.");
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
//Using annotation validation here on the POJO
#Data
#Table
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotNull
#Length(max = 6)
private String firstName;
#NotNull
private String lastName;
private String username;
#Email
private String email;
private LocalDate birthDate;
private String password;
public User() {
}
public User(String firstName, String lastName, String username, String email, String password,
LocalDate birthDate) {
this.firstName = firstName;
this.lastName = lastName;
this.username = username;
this.email = email;
this.password = password;
this.birthDate = birthDate;
}
}
You can create a DTO object for Entity and do all validation there.
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class UserDto {
#NotNull
#Length(max = 6)
private String firstName;
#NotNull
private String lastName;
private String username;
#Email
private String email;
private LocalDate birthDate;
private String password;
}
Then put this object in Controller:
#PostMapping
public ResponseEntity addUser(#Valid #RequestBody UserDto userDto) { // #Valid will check if validation is ok in object UserDto
UserValidation.ValidationResult result = userValidationService.validate(userDto);
if (result.equals(UserValidation.ValidationResult.SUCCESS)){
logger.info("Added a new user.");
userService.addUser(toEntity(userDto)); //maps UserDto to User Entity
return ResponseEntity.ok(HttpStatus.OK);
}
logger.info("Could not add new user.");
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
// maps UserDto to User so you can save it to database in Service layer
User toEntity(UserDto userDto) {
User user = new User();
user.setFirstName(userDto.getFirstName);
user.setLastName(userDto.getUserlastName);
user.setUsername(userDto.getUsername);
user.setEmail(userDto.getEmail);
user.setPassword(userDto.getPassword);
user.setBirthDate(userDto.birthDate)
return user;
}
I have two classes User.java and Address.java and there is a one-to-one bi-directional mapping between them.
But when I try to get the address using the User class I get an "java.lang.StackOverflowError: null" exception.
The same thing happens when I try to get the User from the Address class.
User.java
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String email;
private String phone;
private String password;
private String imageUrl;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "address")
private Address address;
Address.java
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToOne(cascade = CascadeType.ALL, mappedBy = "address")
private User user;
private String country;
private String state;
private String city;
private String street;
private String pincode;
MainController.java
#Controller
public class MainController {
#Autowired
private UserDao userDao;
#Autowired
private AddressDao addressDao;
#RequestMapping("/test")
#ResponseBody
public String test() {
User user = new User();
user.setName("name");
user.setEmail("email");
user.setPhone("phone");
user.setPassword("password");
user.setImageUrl("imageUrl");
Address address = new Address();
address.setCountry("country");
address.setState("state");
address.setCity("city");
address.setStreet("street");
address.setPincode("123456");
user.setAddress(address);
userDao.save(user);
return "working";
}
#RequestMapping("/fetch")
#ResponseBody
public String fetch() {
User user = userDao.getById((long) 1);
System.out.println(user.getAddress());
return "working";
}
}
I am using the test() function to put data in the database and it is working fine.
database image
But when I call the fetch() function I am getting the following error
java.lang.StackOverflowError: null
at org.hibernate.proxy.pojo.BasicLazyInitializer.invoke(BasicLazyInitializer.java:58) ~[hibernate-core-5.6.5.Final.jar:5.6.5.Final]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:43) ~[hibernate-core-5.6.5.Final.jar:5.6.5.Final]
at
Updated MainController.java
package com.demo.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.demo.dao.AddressDao;
import com.demo.dao.UserDao;
import com.demo.entity.Address;
import com.demo.entity.User;
#Controller
public class MainController {
#Autowired
private UserDao userDao;
#Autowired
private AddressDao addressDao;
#RequestMapping("/test")
#ResponseBody
public String test() {
User user = new User();
user.setName("name");
user.setEmail("email");
user.setPhone("phone");
user.setPassword("password");
user.setImageUrl("imageUrl");
userDao.save(user);
Address address = new Address();
address.setCountry("country");
address.setState("state");
address.setCity("city");
address.setStreet("street");
address.setPincode("123456");
addressDao.save(address);
user.setAddress(address);
userDao.save(user);
return "working";
}
#RequestMapping("/fetch")
#ResponseBody
public String fetch() {
Optional<User> op = userDao.findById((long) 1);
User user = op.get();
// working
System.out.println(user.getName() + " " + user.getEmail() + " " + user.getPhone());
// java.lang.StackOverflowError:null
System.out.println(user.getAddress());
return "working";
}
}
TLDR: you aren't actually saving anything anywhere, but it's easy to fix. Here's my code and my explanation:
MainController.java:
#RestController
public class MainController {
private final UserRepository userRepository;
private final AddressRepository addressRepository;
public MainController(UserRepository userRepository, AddressRepository addressRepository){
this.userRepository = userRepository;
this.addressRepository = addressRepository;
}
#GetMapping("/test")
public String test() {
User user = new User();
user.setName("name");
user.setEmail("email");
user.setPhone("phone");
user.setPassword("password");
user.setImageUrl("imageUrl");
user = userRepository.save(user);
System.out.println("saved user");
Address address = new Address();
address.setCountry("country");
address.setState("state");
address.setCity("city");
address.setStreet("street");
address.setPincode("123456");
address = addressRepository.save(address);
System.out.println("saved address");
user.setAddress(address);
userRepository.save(user);
System.out.println("set user's address");
return "working";
}
#GetMapping("/fetch")
public String fetch() {
Optional<User> optionalUser = userRepository.findById((long) 1);
if(optionalUser.isPresent()){
User user = optionalUser.get();
System.out.println(user.getAddress());
boolean addressExists = addressRepository.existsById((long) 1);
System.out.println(addressExists);
System.out.println(user.getAddress().getCountry());
return "working";
}
System.out.println("Error: user with id 1 not found!");
return "failing";
}
}
User.java:
#Entity
#Table(name = "user")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String email;
private String phone;
private String password;
private String imageUrl;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
//getters and setters omitted for brevity
}
Address.java:
#Entity
#Table(name = "address")
public class Address {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToOne(mappedBy = "address")
private User user;
private String country;
private String state;
private String city;
private String street;
private String pincode;
//getters and setters omitted for brevity
}
AddressRepository.java:
public interface AddressRepository extends CrudRepository<Address, Long> {
}
UserRepository.java:
public interface UserRepository extends CrudRepository<User, Long> {
}
UserDAO.java:
public class UserDAO {
private final String name;
private final String email;
private final String phone;
private final String imageUrl;
public UserDAO(User user) {
name = user.getName();
email = user.getEmail();
phone = user.getPhone();
imageUrl = user.getImageUrl();
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public String getPhone() {
return phone;
}
public String getImageUrl() {
return imageUrl;
}
}
A DAO has no connection to the database, it's intent is what the acronym stands for, simply to transfer data, and that's it. When you make a repository, you can stick your objects there by saving them in the repository. Notice that by extending the CrudRepository with correct generics, you don't even need to implement the methods yourself. The save method actually saves the POJO, and returns the saved version, which is why I did user = userRepository.save(user), which may seem counterintuitive at first, but it simply helps ensure that everything is as you expect. If you then want to send the UserDAO object as a response, you can create it using the user object that is returned from the database, maybe something like:
UserDAO dao = new UserDAO(userRepository.save(user));
Please take notice of what is happening inside the test method in MainController. First, we create the POJO User object and set its fields. Then we have to save it to the repository, it is only persisted after you call save method of the repository. Please note that the user object is saved again once it is updated with the address.
This is a very crude way to do things, it is best to create a service layer and do this there with the #Transactional annotation, which would mean that everything is rolled back in case something goes wrong inside a method annotated as #Transactional.
Also, using CascadeType.ALL may be not what you want, please refer to this answer.
Inside fetch method, I ensure that the user indeed exists, which is not guaranteed. To avoid 500 errors, it's important to have a fallback mechanism for when something doesn't work.
As a final side note, you shouldn't be storing raw passwords like that, you should at least use hashing with salt and pepper, or use one of the many available libraries for implementing such functionality (although it can be quite fun getting down and dirty with the code itself). You should also consider how much information you are revealing when something does go wrong, as you don't want to give away too much information which could be used to deanonimise a specific user, or even learn more about your code and the system architecture.
I'm trying to exclude the possibility of a json field to be modificated at HTTP.POST operation. This is my class:
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class UserModel {
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private Long userId;
#NotNull
private String username;
private RoleModel role;
#NotNull
private String email;
#NotNull
private String firstName;
#NotNull
private String secondName;
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private Date registrationDate;
}
I want for example the property userId to be accessible only for read (http get).
I've tried with #JsonProperty but it doesn't work, instead it works for the password field. (this property is visible only for write/ post).
Can you please tell me where I'm wrong? or if there is a more elegant way to do that?
Many thanks,
You can achieve such thing with #JsonView annotation:
// Declare views as you wish, you can also use inheritance.
// GetView also includes PostView's fields
public class View {
interface PostView {}
interface GetView extends PostView {}
}
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class UserModel {
#JsonView(View.GetView.class)
private Long userId;
#JsonView(View.PostView.class)
#NotNull
private String username;
....
}
#RestController
public class Controller {
#JsonView(View.GetView.class)
#GetMapping("/")
public UserModel get() {
return ... ;
}
#JsonView(View.PostView.class)
#PostMapping("/")
public UserModel post() {
return ... ;
}
...
}
For more information: https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring
So lets say I have User object like this
#Entity
public class User {
#Id
#GeneratedValue
private long id;
private String name;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "address", referencedColumnName = "id")
private Address address;
}
#Entity
public class Address {
#Id
#GeneratedValue
private long id;
private String city;
private String country;
}
Now I don't want to write validation annotations in entities. What I would like to do is validate User in #RestController like this
#RestController
public class InvoiceController {
#RequestMapping(value="/users/add", method = RequestMethod.POST)
public Invoice addInvoice(#Validated #RequestBody ValidUser user) {
... do stuff
}
}
The validation annotations would be in ValidUser being like this.
public class ValidUser extends User {
#NotNull
private String name;
#Valid
private Address address;
}
public class ValidAddress extends Address{
#NotNull
private String city;
#NotNull
private String country;
}
The validation works when I remove the address field from the ValidUser but not when it is there. How can I make address validation also work?
I annotated class User with #JsonView and when it returned I see all fields even than that not contains in view class. Here is my class
#Entity
#Table(name = "users")
public class User implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.AUTO)
private Long userID;
#JsonView(View.Summary.class)
#Column(name="email")
private String email;
#JsonView(View.Summary.class)
#Column(name="user_name")
private String firstName;
#JsonView(View.Summary.class)
#Column(name="user_last_name")
private String lastName;
#JsonView(View.Summary.class)
#Column(name="phone")
private String phone;
#JsonView(View.Summary.class)
#Column(name="origin")
private String address;
#JsonView(View.Summary.class)
#Column(name="birth_date")
private Long birthDate;
#JsonView(View.Summary.class)
#Column(name="gender")
private Long gender;
#JsonView(View.Summary.class)
#Column(name="about_me")
private String aboutMe;
#JsonView(View.SummaryWithPhoto.class)
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name="photo")
private Photo avatar;
#JsonView(View.SummaryWithSession.class)
#Transient
private UserSession session;
//getters and setters
Here is my View class
public class View {
public interface Summary {}
public interface SummaryWithPhoto extends Summary {}
public interface SummaryWithSession extends SummaryWithPhoto {}
}
SO then I request get method with #JsonView(View.SummaryWithPhoto.class) annotation I always get userID field but shouldn't. Here is endpoint code
#JsonView(View.SummaryWithPhoto.class)
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<User> getUser(#RequestHeader(value="Access-key") String accessKey,
#RequestHeader(value="Secret-key") String secretKey)
I've spend a some debugging time with the same issue.
Results are:
all fields are included by default if you do not change this behavior (see BeanSerializerFactory.processViews). To change default do:
ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
fields, marked by #JsonView omitted in result if controller method annotated with OTHER #JsonView (see FilteredBeanPropertyWriter.serializeAsField)
So for your use-case do not change default settings, annotate Long userID by #JsonView and getUser by any other (not the same) View.
Code com\fasterxml\jackson\core\jackson-databind\2.8.4\jackson-databind-2.8.4-sources.jar!\com\fasterxml\jackson\databind\MapperFeature.java
* Feature is enabled by default.
*/
DEFAULT_VIEW_INCLUSION(true)
is contradicted of blog https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring, so I have to look at code closer.