Java JSON - Override #JsonIgnore for tests - java

I have project in Spring Boot. I have User model, what have Profile model in relation OneToOne:
User: (Simplified)
#Entity
#Table(name = "users")
public class User extends AbstractEntity {
#Id
#GeneratedValue
private Integer id;
#NotEmpty
#Basic(optional = false)
#Column(nullable = false, unique = true)
private String username;
#Valid
#JsonIgnore
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = false)
private Profile profile;
#JsonIgnore
public Profile getProfile() {
return profile;
}
#JsonProperty
public void setProfile(Profile profile) {
this.profile = profile;
}
}
Profile: (Simplified)
#Entity
#Table(name = "profiles")
public class Profile extends AbstractEntity {
#Id
#GeneratedValue
private Integer id;
#NotEmpty
#Basic(optional = false)
#Column(nullable = false)
private String name;
#NotEmpty
#Basic(optional = false)
#Column(nullable = false)
private String surname;
// Getters, setters, etc
}
My test:
#Test
public void createUserAndProfileReturnsCreatedStatus() throws Exception {
final User user = Generator.generateUser();
user.setProfile(Generator.generateProfile());
MvcResult mvcResult = this.mockMvc.perform(
post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(toJson(user)))
.andExpect(status().isCreated())
.andReturn();
}
Problem is, when i do user.setProfile(), Profile is set into User but when i call toJson(user) its automatically ignored because of my annotations in model.
How to disable those annotations just for purpose of testing? Is it possible?
I dont want to remove #JsonIgnore annotations from model, because they are there to not expose Profile when I READ user by GET /users/<id>.

This can be achieved by utilizing Jackson's Mixin feature, where you create another class that cancels the ignore annotation. The only requirement from the mixin class is to have the same property name and type. class name is not important, nor do you need to instantiate it:
public class DoNotIgnoreProfile
{
#JsonIgnore(false)
private Profile profile;
}
a Jackson Module is required to tie the bean and mixin together:
#SuppressWarnings("serial")
public class DoNotIgnoreProfileModule extends SimpleModule
{
public DoNotIgnoreProfileModule() {
super("DoNotIgnoreProfileModule");
}
#Override
public void setupModule(SetupContext context)
{
context.setMixInAnnotations(User.class, DoNotIgnoreProfile.class);
}
}
now you need to register the module into an ObjectMapper and you're all set:
public string toJson(User user)
{
try {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new DoNotIgnoreProfileModule());
return mapper.writeValueAsString(user);
} catch (IOException e) {
e.printStackTrace();
}
}
EDIT:
I just saw that ObjectMapper has an addMixin() method so the whole module setup can be skipped

Related

Error when mapping classes with attribute that has foreign key

I have the Hardware entity, HardwareDtoRequest and HardwareDtoResponse classes, where I'm using the modelMapper to map them. In the Hardware table, there is a foreign key to the Provider table.
The problem is that I am not able to map this attribute to HardwareDtoRequest, when I call the POST method in Postman passing only the provider_id in the request body it saves only one record with that particular ID, when trying to save again another record with the same ID it updates the old one.
How do I map this foreign key attribute to the DtoRequest and save?
Hardware.java
#Getter
#Setter
#Entity
public class Hardware {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
#ManyToOne
#JoinColumn(name = "provider_id")
private Provider provider;
}
Provider.java
#Getter
#Setter
#Entity
public class Provider {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
}
HardwareDtoRequest.java
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class HardwareDtoRequest {
#NotNull(message = "required field")
private String name;
#NotNull(message = "required field")
private Long providerId;
}
HardwareDtoResponse.java
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class HardwareDtoResponse {
private Long id;
private String name;
private ProviderDtoResponse provider;
}
HardwareMapper.java
public HardwareDtoResponse toHardwareDtoResponse(Hardware hardware) {
return mapper.map(hardware, HardwareDtoResponse.class);
}
public Hardware toHardware(HardwareDtoRequest hardwareDtoRequest) {
return mapper.map(hardwareDtoRequest, Hardware.class);
}
HardwareService.java
#Transactional
public HardwareDtoResponse save(HardwareDtoRequest hardwareDtoRequest) {
Hardware hardware = mapper.toHardware(hardwareDtoRequest);
Hardware saveHardware = hardwareRepository.save(hardware);
return mapper.toHardwareDtoResponse(saveHardware);
}
HardwareController.java
#PostMapping
public ResponseEntity<HardwareDtoResponse> save(#Valid #RequestBody HardwareDtoRequest hardwareDtoRequest) {
log.info("saving hardware: {}", hardwareDtoRequest);
HardwareDtoResponse hardware = hardwareService.save(hardwareDtoRequest);
return new ResponseEntity<>(hardware, HttpStatus.CREATED);
}
I managed to solve it, for those who have the same problem of mapping dtos with modelMapper, I use the following snippet in ModelMapperConfig:
#Configuration
public class ModelMapperConfig {
#Bean
public ModelMapper mapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
return modelMapper;
}
}
You can try to set provider manually. Like this:
public Hardware toHardware(HardwareDtoRequest hardwareDtoRequest) {
Hardware hardware = mapper.map(hardwareDtoRequest, Hardware.class);
Provider provider = providerRepository.findById(hardwareDtoRequest.providerId);
hardware.setProvider(provider);
return hardware;
}

Spring Boot Entity to DTO ConverterNotFoundException

I have a simple API, built with Spring Boot, where I am trying to convert entity classes to the corresponding dtos.
Student Entity
#Entity(name = "students")
#AllArgsConstructor
#Getter
#Setter
public class Student extends AbstractUpdatable<Long> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "student_id")
private Long id;
#Column(name = "student_first_name", nullable = false)
#NotNull(message = "First name is mandatory")
private String firstName;
#Column(name = "student_last_name", nullable = false)
#NotNull(message = "Last name is mandatory")
private String lastName;
#Column(name = "student_ssn", unique = true, nullable = false)
#NotNull(message = "SSN is mandatory")
#Size(min = 10, max = 10)
private String ssn;
#Column(name = "student_age")
#Min(5)
#Max(100)
private Integer studentAge;
#Column(name = "student_email")
#Email
private String email;
#Column(name = "student_level")
private Integer studentLevel; // TODO could be enum or separate entity
#Column(name = "student_creation_date")
private Date creationDate; // TODO check spring's feature to manage creation and update dates (auditing)
#ManyToOne
#JoinColumn(name = "group_id")
private Group group;
public Student() {
this.creationDate = new java.util.Date(); // TODO this will be removed when spring's auditing is utilized
}
}
Group Entity
#Entity(name = "groups")
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Setter
public class Group extends AbstractUpdatable<Long> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "group_id")
private Long id;
#Column(name = "group_name")
private String Name;
#OneToMany(mappedBy = "group")
#JsonIgnore
private List<Student> students;
}
Student DTO
public class StudentDto extends AbstractStudentDto implements Serializable {
private final Long id;
private final Date creationDate;
public StudentDto(String firstName, String lastName, String email, Long id, GroupDto group, Integer studentAge, Integer studentLevel,
Date creationDate) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.group = group;
this.studentAge = studentAge;
this.studentLevel = studentLevel;
this.creationDate = creationDate;
}
public Long getId() {
return id;
}
public Date getCreationDate() {
return creationDate;
}
}
Group DTO
public class GroupDto extends AbstractGroupDto{
private final Long id;
public GroupDto(Long id, String name, List<StudentDto> students) {
this.id = id;
this.name = name;
this.students = students;
}
public Long getId() {
return id;
}
}
GroupToGroupDtoConverter
#Component
public class GroupToGroupDtoConverter implements org.springframework.core.convert.converter.Converter<Group, GroupDto> {
private final ConversionService conversionService;
#Autowired
public GroupToGroupDtoConverter(ConversionService conversionService) {
this.conversionService = conversionService;
}
#Override
public GroupDto convert(Group source) {
var convertedStudents = new ArrayList<StudentDto>();
if (!CollectionUtils.isEmpty(source.getStudents())) {
source.getStudents().forEach(student ->
convertedStudents.add(conversionService
.convert(student, StudentDto.class)));
}
return new GroupDto(source.getId(), source.getName(), convertedStudents);
}
}
And a very similar StudentToStudentDtoConverter.
The issue is that when the code needs to do the conversion from any of the entities to their dtos I get
org.springframework.core.convert.ConverterNotFoundException: No
converter found capable of converting from type
[com.studentmanagement.model.Group] to type
[com.studentmanagement.dto.group.GroupDto]
Now if I try to remove the conversion of the students' list to a list of student dtos in the converter above, so the converter looks like this:
#Component
public class GroupToGroupDtoConverter implements org.springframework.core.convert.converter.Converter<Group, GroupDto> {
#Override
public GroupDto convert(Group source) {
return new GroupDto(source.getId(), source.getName(), new ArrayList<StudentDto>());
}
}
The conversion works with no issues (with a dummy students list of course). Am I missing something when I am adding the conversion service inside my converters?
I tried to replicate the issue and did a small working demo on this.
One thing I found while doing that, that could be relevant to your case, is that injecting a conversion service into a converter is not trivial (see e.g. this and this relevant issues).
Also, important as well, do not forget to register the converters, as shown below on the code samples, and e.g. here. From the error message you posted, seems like the service cannot find the needed converter.
For my demo, please note I removed the group field from the StudentDTO class to simplify things. I hope it helps, happy to share the full code as well in github.
I used the following converter for Group:
#Component
public class GroupToGroupDtoConverter implements org.springframework.core.convert.converter.Converter<Group, GroupDto> {
private ConversionService conversionService;
public GroupToGroupDtoConverter(ConversionService conversionService) {
this.conversionService = conversionService;
}
#Override
public GroupDto convert(Group group) {
List<StudentDto> studentDtoList =
group.getStudents().stream().map(a -> conversionService.convert(a, StudentDto.class)).collect(Collectors.toList());
GroupDto groupDto = new GroupDto(group.getId(), group.getName(), studentDtoList);
return groupDto;
}
}
But in order to successfully inject the conversion service and register the converters I added this:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Autowired
#Lazy
ConversionService conversionService;
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new GroupToGroupDtoConverter(conversionService));
registry.addConverter(new StudentToStudentDtoConvervter());
}
}
If, for example, I comment out the first addConverter line, I get the Converter not found exception:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.example.conversion_entities.Group] to type [com.example.conversion_entities.GroupDto]
Once all is done, the following test passes:
#RunWith(SpringRunner.class)
#SpringBootTest
class GroupToGroupDtoConverterTest {
#Autowired
ConversionService conversionService;
#Test
void convert() {
Student studentA = new Student();
studentA.setFirstName("John");
studentA.setLastName("Doe");
Student studentB = new Student();
studentB.setFirstName("Jane");
studentB.setLastName("Doe");
List<Student> studentList = new ArrayList<>();
studentList.add(studentA);
studentList.add(studentB);
Group group = new Group(1L, "groupA", studentList);
GroupDto convertedGroupDto = conversionService.convert(group, GroupDto.class);
assertEquals("John", convertedGroupDto.getStudents().get(0).getFirstName());
assertEquals("Jane", convertedGroupDto.getStudents().get(1).getFirstName());
}
}
I think the problem is ConversionService can't convert your classes by default. Instead try to inject a class implementing ConversionService with correct convert method implementation inside.

Getting nulls while using ModelMapper

I'm trying to utilize the ModelMapper in my convertion process. What I need to do is to convert the Sample entity to SampleDTO object.
I have the Sample entity like the following:
#Entity
#Table(name = "sample", schema = "sample_schema")
#Data
#NoArgsConstructor
public class Sample {
private static final String SEQUENCE = "SAMPLE_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "name")
private String name;
#Column
private String surname;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "id_deetails")
private Details details;
}
Which holds the Details one:
#Entity
#Table(name = "details", schema = "sample_schema")
#Data
#NoArgsConstructor
public class Details {
private static final String SEQUENCE = "DETAILS_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "street_name")
private String streetName;
#Column
private String city;
}
I'd like the DTO to be this format:
#NoArgsConstructor
#AllArgsConstructor
#Data
public class SampleDTO {
private Long id;
private String name;
private String surname;
private String streetName;
private String city;
}
I also made a ModelMapper bean like:
#Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
And I made a converter component:
#Component
public class EntityDtoConverter {
private final ModelMapper modelMapper;
#Autowired
public EntityDtoConverter(ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
public SampleDTO sampleToDto(Sample entity) {
return modelMapper.map(entity, SampleDTO.class);
}
}
The problem is
when I try to use this mapper converter in my service
#Service
public class SampleService {
private final SampleRepository sampleRepository;
private final EntityDtoConverter entityDtoConverter;
#Autowired
public SampleService(SampleRepository sampleRepository, EntityDtoConverter entityDtoConverter) {
this.sampleRepository = sampleRepository;
this.entityDtoConverter = entityDtoConverter;
}
public List<SampleDTO> getSamples() {
List<SampleDTO> samples = sampleRepository.findAll()
.map(entityDtoConverter::sampleToDto);
return new List<SampleDTO>(samplesPage);
}
}
I get nulls in places of Details fields.
I have followed Baeldung's tutorial about model-to-dto conversion with ModelMapper and the documentation of it as well but the least wasn't much of help. There is something I'm missing and I have no idea what it is.
I'm working on:
Java 11
Spring Boot 2.3.0
ModelMapper 2.3.8
Try:
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
Also check: Modelmapper: How to apply custom mapping when source object is null?

How to test json api when collection is Set

I read that it is preferable to use the Set instead List in Hibernate relation.
I created two entities in relation to one to many:
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, unique = true)
private String name;
#ManyToOne
#JoinColumn(name = "company_id", nullable = false)
private Company company;
}
#Entity
public class Company {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, unique = true)
private String name;
#LazyCollection(LazyCollectionOption.TRUE)
#OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
private Set<Product> products;
}
In relation #OneToMany set collection private Set products;
Then I try to test the return result:
#RunWith(SpringRunner.class)
#SpringBootTest
#Transactional
public class CompanyControllerTest {
private static final String API_COMPANY = "/api/company/";
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#Before
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.build();
}
#Test
public void getById() throws Exception {
int id = 1;
this.mockMvc.perform(get(API_COMPANY + id))
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON_UTF8))
.andExpect(jsonPath("id", is(1)))
.andExpect(jsonPath("$.name", is("Google")))
.andExpect(jsonPath("$.products", hasSize(2)))
.andExpect(jsonPath("$.products[0].id", is(1)))
.andExpect(jsonPath("$.products[0].name", is("search engine")))
.andExpect(jsonPath("$.products[0].company").doesNotExist())
.andExpect(jsonPath("$.products[1].id", is(2)))
.andExpect(jsonPath("$.products[1].name", is("adv.")))
.andExpect(jsonPath("$.products[1].company").doesNotExist());
}
}
But the problem is that the list of products is constantly changing, because I use a Set.
And it turns out that the test either passes or fails, since the order of the products changes.
My question is how to test the result when using Set.
You can get all the elements with [*] and give a Matchers.containsInAnyOrder(T...) all the elements that you want to check.
Something like this:
this.mockMvc.perform(get(API_COMPANY + id))
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON_UTF8))
.andExpect(jsonPath("id", is(1)))
.andExpect(jsonPath("$.name", is("Google")))
.andExpect(jsonPath("$.products", hasSize(2)))
.andExpect(jsonPath("$.products[*].id", Matchers.containsInAnyOrder(1, 2)))
.andExpect(jsonPath("$.products[*].name", Matchers.containsInAnyOrder("search engine", "adv.")))
.andExpect(jsonPath("$.products[*].company").doesNotExist());

Form Validation in Rest Webservice

I started building my first REST webservice in Java using Spring and JPA.
Now I'm trying to create sign-up service. I have no problem with sending a request containing all Entity fields what looks:
#AllArgsConstructor
#NoArgsConstructor
#Data
#Builder
#Entity
#Table(name = "users")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Enumerated(EnumType.STRING)
private Gender gender;
#Column(name = "email")
private String email;
#Column(name = "login")
private String login;
#Column(name = "password")
private String password;
#Column(name = "registration_date")
#CreatedDate
private LocalDateTime registrationDate;
#OneToMany(mappedBy = "bookOwner", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Book> bookList = new ArrayList<>();
}
But what to do in situation I want my registration form having only login, password and email fields and filling the rest user details would be optional - after confirmation of registration?
I consider using ModelMapper and creating separate classes for every form, but is there any better approach?
I solved problem by my own using mentioned ModelMapper. I paste my code. Can be useful if someone's interested. Didn't make tests, but my DB looks fine and no exceptions are thrown.
public class DTOMapper {
private static final ModelMapper MAPPER = new ModelMapper();
private DTOMapper(){}
public static <S, T> T map(S source, Class<T> targetClass){
return MAPPER.map(source, targetClass);
}
}
#Service
#Transactional
public class SignUpService {
private final UserRepository userRepository;
#Autowired
public SignUpService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User registerUser(SignUpForm form){
if(userRepository.findByLogin(form.getLogin())!=null){
throw new LoginAlreadyUsedException(form.getLogin());
}
if(userRepository.findByEmail(form.getEmail())!=null){
throw new EmailAlreadyUsedException(form.getEmail());
}
User user = DTOMapper.map(form, User.class);
User saved = userRepository.save(user);
return DTOMapper.map(saved, User.class);
}
}
#AllArgsConstructor
#NoArgsConstructor
#Data
#Builder
public class SignUpForm implements Serializable {
private static final long serialVersionUID = 1L;
#NotEmpty
#Size(min = 5)
private String login;
#NotEmpty
#Size(min = 7)
private String password;
//todo email validation
#NotEmpty
private String email;
}
#RestController
public class SignUpController {
private static final Logger log = LoggerFactory.getLogger(SignUpController.class);
#Autowired
private SignUpService signUpService;
#PostMapping(value = "/signup")
public ResponseEntity<?> addUser(#RequestBody #Valid SignUpForm form, BindingResult errors){
if(errors.hasErrors()){
throw new InvalidRequestException(errors);
}
signUpService.registerUser(form);
return new ResponseEntity<>(form, HttpStatus.CREATED);
}
}

Categories