I am building a simple REST service using spring. I separated my entities from DTOs and I made the DTOs immutable using Immutables. I needed mapping between DTOs and DAOs, so I chose MapStruct. The Mapper is not able to detect the setters I have defined in my DAOs.
The problem is exactly similar to this question. This question does not have an accepted answer and I have tried all of the suggestions in that question and they don't work. I don't want to try this answer because I feel it defeats the purpose for which I am using Immutables. #marc-von-renteln summarizes this reason nicely in the comment here
I tried the answer provided by #tobias-schulte. But that caused a different problem. In the Mapper class in the answer, trying to return Immutable*.Builder from the mapping method throws an error saying the Immutable type cannot be found.
I have exhaustively searched issues logged against MapStruct and Immutables and I haven't been able to find a solution. Unfortunately there are hardly few examples or people using a combination of MapStruct and Immutables. The mapstruct-examples repository also doesn't have an example for working with Immutables.
I even tried defining separate Mapper interfaces for each of the DtTOs (like UserStatusMapper). I was only making it more complicated with more errors.
I have created a sample spring project to demonstrate the problem.
GitHub Repo Link. This demo app is almost same as the REST service I am creating. All database (spring-data-jpa , hibernate) stuff is removed and I am using mock data.
If you checkout the project and run the demo-app you can make two API calls.
GetUser:
Request:
http://localhost:8080/user/api/v1/users/1
Response:
{
"id": 0,
"username": "TestUser",
"email": "TestUser#demo.com",
"userStatus": {
"id": 1,
"status": 1,
"statusName": "Active"
}
Createuser: PROBLEM HERE
http://localhost:8080/user/api/v1/users/create
Sample Input:
{
"username": "TestUser",
"email": "TestUser#demo.com",
"userStatus": {
"id": 1,
"status": 1,
"statusName": "Active"
}
}
Response:
{
"timestamp": "2019-04-28T09:29:24.933+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class com.immutablesmapstruct.demo.dto.model.ImmutableUserDto$Builder]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.immutablesmapstruct.demo.dto.model.ImmutableUserDto$Builder`, problem: Cannot build UserDto, some of required attributes are not set [username, email, userStatus]\n at [Source: (PushbackInputStream); line: 9, column: 1]",
"path": "/user/api/v1/users/create"
}
Below are important pieces of code related to problem:
Daos:
1. UserDao
public class User {
// Primary Key. Something that is annotated with #Id
private int id;
private String username;
private String email;
private UserStatus userStatus;
private User(Builder builder) {
id = builder.id;
username = builder.username;
email = builder.email;
userStatus = builder.userStatus;
}
public static Builder builder() {
return new Builder();
}
public int getId() {
return id;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
public UserStatus getUserStatus() {
return userStatus;
}
public static final class Builder {
private int id;
private String username;
private String email;
private UserStatus userStatus;
private Builder() {
}
public Builder setId(int id) {
this.id = id;
return this;
}
public Builder setUsername(String username) {
this.username = username;
return this;
}
public Builder setEmail(String email) {
this.email = email;
return this;
}
public Builder setUserStatus(UserStatus userStatus) {
this.userStatus = userStatus;
return this;
}
public User build() {
return new User(this);
}
2. UserStatusDao:
package com.immutablesmapstruct.demo.dao.model;
/**
* Status of user.
* Example: Active or Inactive
*/
public class UserStatus {
// Primary Key. Something that is annotated with #Id
private int id;
// A value of 1 or 0
private int status;
// Active , InActive
private String statusName;
private UserStatus(Builder builder) {
id = builder.id;
status = builder.status;
statusName = builder.statusName;
}
public static Builder builder() {
return new Builder();
}
public int getId() {
return id;
}
public int getStatus() {
return status;
}
public String getStatusName() {
return statusName;
}
public static final class Builder {
private int id;
private int status;
private String statusName;
private Builder() {
}
public Builder setId(int id) {
this.id = id;
return this;
}
public Builder setStatus(int status) {
this.status = status;
return this;
}
public Builder setStatusName(String statusName) {
this.statusName = statusName;
return this;
}
public UserStatus build() {
return new UserStatus(this);
}
}
}
DTOs
1. UserDto:
package com.immutablesmapstruct.demo.dto.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
#Value.Immutable
#Value.Style(defaults = #Value.Immutable(copy = false), init = "set*")
#JsonSerialize(as = ImmutableUserDto.class)
#JsonDeserialize(builder = ImmutableUserDto.Builder.class)
public abstract class UserDto {
#Value.Default
#JsonProperty
public int id() {
return 0;
}
#JsonProperty
public abstract String username();
#JsonProperty
public abstract String email();
#JsonProperty
public abstract UserStatusDto userStatus();
2. UserStatusDto:
package com.immutablesmapstruct.demo.dto.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
#Value.Immutable
#Value.Style(defaults = #Value.Immutable(copy = false), init = "set*")
#JsonSerialize(as = ImmutableUserStatusDto.class)
#JsonDeserialize(builder = ImmutableUserStatusDto.Builder.class)
public abstract class UserStatusDto {
#JsonProperty
public abstract int id();
#JsonProperty
public abstract int status();
#JsonProperty
public abstract String statusName();
}
MapStruct UserMapper:
package com.immutablesmapstruct.demo.dto.mapper;
import com.immutablesmapstruct.demo.dao.model.User;
import com.immutablesmapstruct.demo.dao.model.UserStatus;
import com.immutablesmapstruct.demo.dto.model.UserDto;
import com.immutablesmapstruct.demo.dto.model.UserStatusDto;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
#Mapper(componentModel = "spring")
public interface UserMapper {
UserMapper USER_MAPPER_INSTANCE = Mappers.getMapper(UserMapper.class);
UserDto userDaoToDto(User user);
//Problem here.
User userDtoToDao(UserDto userDto);
UserStatusDto userStatusDaoToDto(UserStatus userStatusDao);
UserStatus userStatusDtoToDao(UserStatusDto userStatusDto);
}
If I look at the concrete method generated by MapStruct for userDtoToDao I can clearly see that the setters are not being recognized.
package com.immutablesmapstruct.demo.dto.mapper;
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2019-04-28T02:29:03-0700",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_191 (Oracle Corporation)"
)
#Component
public class UserMapperImpl implements UserMapper {
...
...
#Override
public User userDtoToDao(UserDto userDto) {
if ( userDto == null ) {
return null;
}
com.immutablesmapstruct.demo.dao.model.User.Builder user = User.builder();
return user.build();
}
....
....
}
Mapstruct doesn't recognize your getters in UserDto and UserStatusDto.
When you change the existing methods (like public abstract String username()) in these abstract classes to classic getters like
#JsonProperty("username")
public abstract String getUsername();
the MapperImpl will contain the required calls. Note, that the #JsonProperty needs to have the attributes name itself afterwards (because of the changed method name).
Here are the complete classes UserDto and UserStatusDto with said changes:
package com.immutablesmapstruct.demo.dto.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
#Value.Immutable
#Value.Style(defaults = #Value.Immutable(copy = false), init = "set*")
#JsonSerialize(as = ImmutableUserDto.class)
#JsonDeserialize(builder = ImmutableUserDto.Builder.class)
public abstract class UserDto {
#Value.Default
#JsonProperty("id")
public int getId() {
return 0;
}
#JsonProperty("username")
public abstract String getUsername();
#JsonProperty("email")
public abstract String getEmail();
#JsonProperty("userStatus")
public abstract UserStatusDto getUserStatus();
}
package com.immutablesmapstruct.demo.dto.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.immutables.value.Value;
#Value.Immutable
#Value.Style(defaults = #Value.Immutable(copy = false), init = "set*")
#JsonSerialize(as = ImmutableUserStatusDto.class)
#JsonDeserialize(builder = ImmutableUserStatusDto.Builder.class)
public abstract class UserStatusDto {
#JsonProperty("id")
public abstract int getId();
#JsonProperty("status")
public abstract int getStatus();
#JsonProperty("statusName")
public abstract String getStatusName();
}
I created a repository with the following method.
#Modifying(clearAutomatically = true)
#Query(
value = "UPDATE address SET address_line_1 = :#{#address.getAddressLine1()} , address_line_2 = :#{#address.getAddressLine2()} ," +
" address_line_3 = :#{#address.getAddressLine3()} , city = :#{#address.getCity()} , address_type = :#{#address.getAddressType().toString()} ," +
" postal_code = :#{#address.getPostalCode()} , state = :#{#address.getState().toString()} , country= :#{#address.getCountry().toString()} , residence_type = :#{#address.getResidenceType().toString()} ," +
" discriminator = :#{#address.getAddressType().toString()},created_by = :#{#address.getCreatedBy()} , created_date = :#{#address.getCreatedDateTime()} WHERE address_id = :#{#address.getId()}",
nativeQuery = true)
void updateAddress(#Param("address") Address address);
During the updates the addressLine2/ addressLine3 is converted to and stored in the database in hexa format.
For example, if addressLine2 is passed into the method as 1OFFICE OF HOBBITS, it is converted to and stored as \x314f6666696365206f6620486f6262697473
This only occurs on a few updates (not all). I cannot discern a distinguishable pattern among the values that are updated as expected and those that are converted to hexa format.
HELP!!
Additional Info:
I even tried without the Spel Expression and I see the same error:
Here are some more details:
Repository Interface:
import com.company.domain.Address;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface AddressRepository extends JpaRepository<Address, Long> {
#Modifying(clearAutomatically = true)
#Query(
value = "UPDATE address SET address_line_1 = :address_line_1 , address_line_2 = :address_line_2 ," +
" address_line_3 = :address_line_3 WHERE address_id = :address_id ",
nativeQuery = true)
void updateAddress(#Param("address_line_1") String address_line_1,#Param("address_line_2") String address_line_2,#Param("address_line_3") String address_line_3,#Param("address_id") Long address_id);
}
Service Class:
import com.company.hibernate.AddressRepository;
import com.company.domain.Address;
import com.company.domain.AddressService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Class by AddressServiceImpl
*/
#Service
public class AddressServiceImpl implements AddressService {
#Autowired
private AddressRepository addressRepository;
#Override
public void updateAddress(Address address) {
System.out.println(address);
System.out.println(address.getId());
System.out.println(address.getAddressLine2());
addressRepository.updateAddress(address.getAddressLine1() , address.getAddressLine2() ,
address.getAddressLine3(),address.getId());
}
}
When I call the service method
addressService.updateAddress(address);
I see the following sysouts:
{"addressId":2000112115,"addressLine1":"2001 Hussle
Road2","addressLine2":"Office of Hobbits","addressLine3":null }
2000112115
Office of Hobbits
But In the database I see : the following for address_line_2
\x4f6666696365206f6620486f6262697473
Updated 2 - Added address class:
package com.company.domain;
import com.company.common.Identifiable;
import com.company.enums.AddressType;
import com.company.ResidenceType;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.EqualsExclude;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.HashCodeExclude;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.io.Serializable;
import java.time.LocalDateTime;
public class Address implements Identifiable<Long>, Serializable {
private static final long serialVersionUID = 599052439022921076L;
protected static final String INVALID_ADDRESS = "InvalidAddress";
private Long addressId;
private String addressLine1;
private String addressLine2;
private String addressLine3;
#Override
public Long getId() {
return addressId;
}
public void setId(Long id) {
this.addressId = id;
}
public Customer getCustomer() {
return customer;
}
public String getAddressLine1() {
return addressLine1;
}
public void setAddressLine1(String addressLine1) {
this.addressLine1 = addressLine1;
}
public String getAddressLine2() {
return addressLine2;
}
public void setAddressLine2(String addressLine2) {
this.addressLine2 = addressLine2;
}
public String getAddressLine3() {
return addressLine3;
}
public void setAddressLine3(String addressLine3) {
this.addressLine3 = addressLine3;
}
#Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj, false);
}
#Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, false);
}
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
}
}
I am getting this exception every time i try to query a view on Couchbase DB from my spring boot application.
Unsupported parameter type for key: class com.couchbase.client.protocol.views.Query.
I was setting a string on setKey() method of Query class, got an exception. But then I checked the API and provided a json to setKey, still not working. Have searched a lot but could not get this to work.
I am sharing the code snippet in this post as well.
Application.properties
spring.couchbase.bucket.password=
spring.couchbase.bucket.name=default
spring.couchbase.bootstrap-hosts=127.0.0.1
spring.data.couchbase.repositories.enabled=true
PlayerRepository
public interface PlayerRepository extends CouchbaseRepository<Player, Integer>
{
#View(designDocument = "player", viewName = "all")
public List<Player> findAll();
#View(designDocument = "player", viewName = "by_Name")
public Player findByName(Query query);
#View(designDocument = "player", viewName = "by_TeamId")
public List<Player> findByTeamId(Query query);
}
Player.java
#Document
public class Player
{
#Id
int playerId;
#Field
String name;
#Field
String type;
#Field
String country;
#Field
String playingHand;
#Field
String era;
#Field
int teamId;
#Field
int odiCenturies;
#Field
int testCenturies;
public Player(){}
public Player(int playerId, String name, String type, String country, String playingHand, String era, int teamId,
int odiCenturies, int testCenturies) {
super();
this.playerId = playerId;
this.name = name;
this.type = type;
this.country = country;
this.playingHand = playingHand;
this.era = era;
this.teamId = teamId;
this.odiCenturies = odiCenturies;
this.testCenturies = testCenturies;
}
SpringBootApplication class
#SpringBootApplication
public class CricketTeamSelectionMain
{
/**
* #param args
*/
public static void main(String[] args)
{
SpringApplication.run(CricketTeamSelectionMain.class, args);
}
#Configuration
#EnableCouchbaseRepositories
public static class DBConfig extends AbstractCouchbaseConfiguration
{
#Value("${spring.couchbase.bucket.name}")
private String bucketName;
#Value("${spring.couchbase.bucket.password}")
private String password;
#Value("${spring.couchbase.bootstrap-hosts}")
private String ip;
#Override
public String getBucketName() {
return this.bucketName;
}
#Override
public String getBucketPassword() {
return this.password;
}
#Override
public List<String> getBootstrapHosts() {
return Arrays.asList(this.ip);
}
}
}
PlayerService class
package org.ups.fantasyCricket.service;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.ups.fantasyCricket.CricketTeamSelectionMain.DBConfig;
import org.ups.fantasyCricket.Model.Player;
import org.ups.fantasyCricket.Repository.PlayerRepository;
import com.couchbase.client.CouchbaseClient;
import com.couchbase.client.protocol.views.Query;
import com.couchbase.client.protocol.views.View;
import com.couchbase.client.protocol.views.ViewResponse;
#Service
public class PlayerService
{
#Autowired
PlayerRepository playerRepo;
private CouchbaseClient client;
public List<Player> getAllPlayers()
{
List<Player> allPlayerLists = new ArrayList<Player>();
/*allPlayerLists.addAll((Collection<? extends Player>) playerRepo.findAll());
return allPlayerLists;*/
playerRepo.findAll().forEach(allPlayerLists::add);
return allPlayerLists;
}
public Player getPlayerByName(String name)
{
DBConfig dbCon = new DBConfig();
try
{
Query query = new Query();
query.setIncludeDocs(true);
query.setKey(name);
Player player = playerRepo.findByName(query);
return player;
}
catch(Exception e)
{
e.printStackTrace();
System.out.println(e.getMessage());
}
return null;
}
public String addPlayer(Player player)
{
playerRepo.save(player);
return "Success";
}
public String updatePlayer(Player player, int id)
{
playerRepo.save(player);
return "Success";
}
public List<Player> getPlayersByTeamId(int teamId)
{
List<Player> allPlayerLists = new ArrayList<Player>();
Query query = new Query();
query.setKey(String.valueOf(teamId));
playerRepo.findByTeamId(query).forEach(allPlayerLists::add);
return allPlayerLists;
}
public String addPlayers(List<Player> players)
{
playerRepo.save(players);
return "Success";
}
}
View by_Name on CouchBase DB
function (doc) {
emit(doc.name, doc);
}
Which version of spring-data-couchbase are you using? Starting with 2.x, the #Query annotation uses query derivation and you cannot use a ViewQuery as a parameter anymore... Have a look at the docs, on query derivation with a view.
You could probably use the CouchbaseTemplate to perform a manual query though.
I'm new to Spring, so I apologize if the question is basic.
I'm writing an application which fetches data from postgres and returns as JSON. The table structure is as below:
table student{
id int,
created_time Timestamp
}
I have an entry in the table as:
id | created_time
---+---------------------------
1 | 2015-02-19 23:58:23.579761
I'm having my entity object as:
package main.java;
import java.sql.Timestamp;
import javax.persistence.Entity;
import javax.persistence.Id;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#Entity
#JsonIgnoreProperties(ignoreUnknown = true)
public class Student {
#Id
protected int id;
protected Timestamp created_time;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Timestamp getCreated_time() {
return created_time;
}
public void setCreated_time(Timestamp created_time) {
this.created_time = created_time;
}
}
And this is how I'm returning the data:
#RestController
#RequestMapping(value = "/student", produces = MediaType.APPLICATION_JSON_VALUE)
public class StudentController {
#Autowired
protected StudentRepository studentRepository;
#RequestMapping(value = "/{id}")
public Student student(#PathVariable int id) {
return studentRepository.findOne(id);// uses the findOne() method inherited from CrudRepository
}
However, the json I get is:
{"id":1,"created_time":1424419103579}
Why is it returning junk value for time stamp? How do I get the original value in same format as is in the table?
Thanks in advance.
This is the default behaviour.
By default Jackson uses default strategy to determine the date formatting when serializating into JSON, so you get date in milliseconds.
To override this with custom behavior, you can use #JsonSerialize annotation.
You will need a custom Date serializer, for example
#Component
public class JsonDateSerializer extends JsonSerializer<Timestamp> {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
#Override
public void serialize(Timestamp date, JsonGenerator gen, SerializerProvider provider)
throws IOException, JsonProcessingException {
String formattedDate = dateFormat.format(date);
gen.writeString(formattedDate);
}
}
And then, in your entity you can do
#Entity
#JsonIgnoreProperties(ignoreUnknown = true)
public class Student {
#Id
protected int id;
protected Timestamp created_time;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
#JsonSerialize(using=JsonDateSerializer.class)
public Timestamp getCreated_time() {
return created_time;
}
public void setCreated_time(Timestamp created_time) {
this.created_time = created_time;
}
}
P.S. If you named your field as created_time only to be able to serialize your property into json having created_time as a name, better to use instead #JsonProperty.
#JsonProperty("created_time")
#JsonSerialize(using=JsonDateSerializer.class)
protected Timestamp createdTime;
I'm building a Spring Data REST application and I'm having some problems when I try to POST it. The main entity has other two related entities nested.
There is a "questionary" object which has many answers and each one of these answers have many replies.
I generate a JSON like this from the front application to POST the questionary:
{
"user": "http://localhost:8080/users/1",
"status": 1,
"answers": [
{
"img": "urlOfImg",
"question": "http://localhost:8080/question/6",
"replies": [
{
"literal": "http://localhost:8080/literal/1",
"result": "6"
},
{
"literal": "http://localhost:8080/literal/1",
"result": "6"
}
]
},
{
"img": "urlOfImg",
"question": "http://localhost:8080/question/6",
"replies": [
{
"literal": "http://localhost:8080/literal/3",
"result": "10"
}
]
}
]
}
But when I try to post it, I get the follow error response:
{
"cause" : {
"cause" : {
"cause" : null,
"message" : "Template must not be null or empty!"
},
"message" : "Template must not be null or empty! (through reference chain: project.models.Questionary[\"answers\"])"
},
"message" : "Could not read JSON: Template must not be null or empty! (through reference chain: project.models.Questionary[\"answers\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: project.models.Questionary[\"answers\"])"
}
Edit:
I also add my repository:
#RepositoryRestResource(collectionResourceRel = "questionaries", path = "questionaries")
public interface InspeccionRepository extends JpaRepository<Inspeccion, Integer> {
#RestResource(rel="byUser", path="byUser")
public List<Questionary> findByUser (#Param("user") User user);
}
My Entity Questionary class is :
#Entity #Table(name="QUESTIONARY", schema="enco" )
public class Questionary implements Serializable {
private static final long serialVersionUID = 1L;
//----------------------------------------------------------------------
// ENTITY PRIMARY KEY ( BASED ON A SINGLE FIELD )
//----------------------------------------------------------------------
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEC_QUESTIONARY")
#SequenceGenerator(name = "SEC_QUESTIONARY", sequenceName = "ENCO.SEC_QUESTIONARY", allocationSize = 1)
#Column(name="IDQUES", nullable=false)
private Integer idques ;
//----------------------------------------------------------------------
// ENTITY DATA FIELDS
//----------------------------------------------------------------------
#Column(name="ESTATUS")
private Integer estatus ;
//----------------------------------------------------------------------
// ENTITY LINKS ( RELATIONSHIP )
//----------------------------------------------------------------------
#ManyToOne
#JoinColumn(name="IDUSER", referencedColumnName="IDUSER")
private User user;
#OneToMany(mappedBy="questionary", targetEntity=Answer.class)
private List<Answer> answers;
//----------------------------------------------------------------------
// CONSTRUCTOR(S)
//----------------------------------------------------------------------
public Questionary()
{
super();
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR FIELDS
//----------------------------------------------------------------------
//--- DATABASE MAPPING : IDNSE ( NUMBER )
public void setIdnse( Integer idnse )
{
this.idnse = idnse;
}
public Integer getIdnse()
{
return this.idnse;
}
//--- DATABASE MAPPING : ESTADO ( NUMBER )
public void setEstatus Integer estatus )
{
this.estatus = estatus;
}
public Integer getEstatus()
{
return this.estatus;
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR LINKS
//----------------------------------------------------------------------
public void setUser( Usuario user )
{
this.user = user;
}
public User getUser()
{
return this.user;
}
public void setAnswers( List<Respuesta> answers )
{
this.answers = answer;
}
public List<Answer> getAnswers()
{
return this.answers;
}
// Get Complete Object method public List<Answer>
getAnswerComplete() {
List<Answer> answers = this.answers;
return answers;
}
}
My Answer Entity:
#Entity #Table(name="ANSWER", schema="enco" ) public class Answer
implements Serializable {
private static final long serialVersionUID = 1L;
//----------------------------------------------------------------------
// ENTITY PRIMARY KEY ( BASED ON A SINGLE FIELD )
//----------------------------------------------------------------------
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEC_ANSWER")
#SequenceGenerator(name = "SEC_ANSWER", sequenceName = "ENCOADMIN.SEC_ANSWER", allocationSize = 1)
#Column(name="IDANS", nullable=false)
private Integer idans ;
//----------------------------------------------------------------------
// ENTITY DATA FIELDS
//----------------------------------------------------------------------
#Column(name="IMG", length=100)
private String img ;
//----------------------------------------------------------------------
// ENTITY LINKS ( RELATIONSHIP )
//----------------------------------------------------------------------
#ManyToOne
#JoinColumn(name="IDQUES", referencedColumnName="IDQUES")
private Questionary questionary ;
#OneToMany(mappedBy="answer", targetEntity=Reply.class)
private List<Reply> replies;
#ManyToOne
#JoinColumn(name="IDQUE", referencedColumnName="IDQUE")
private Question Question ;
//----------------------------------------------------------------------
// CONSTRUCTOR(S)
//----------------------------------------------------------------------
public Answer()
{
super();
}
//----------------------------------------------------------------------
// GETTER & SETTER FOR THE KEY FIELD
//----------------------------------------------------------------------
public void setIdans( Integer idans )
{
this.idans = idans ;
}
public Integer getIdans()
{
return this.idans;
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR FIELDS
//----------------------------------------------------------------------
//--- DATABASE MAPPING : IMAGEN ( VARCHAR2 )
public void setImg( String img )
{
this.img = img;
}
public String getImg()
{
return this.img;
}
//----------------------------------------------------------------------
// GETTERS & SETTERS FOR LINKS
//----------------------------------------------------------------------
public void setQuestionary( Questionary questionary )
{
this.questionary = questionary;
}
public Questionary getQuestionary()
{
return this.questionary;
}
public void setReplies( List<Reply> contestaciones )
{
this.replies = replies;
}
public List<Reply> getReplies()
{
return this.replies;
}
public void setQuestion( Question question )
{
this.question = question;
}
public Question getQuestion()
{
return this.question;
}
}
And this is the error console:
Caused by: com.fasterxml.jackson.databind.JsonMappingException:
Template must not be null or empty! (through reference chain:
project.models.Questionary["answers"]) at
com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:232)
~[jackson-databind-2.3.3.jar:2.3.3] at *snip*
Try adding #RestResource(exported = false) on field answers in class Questionary.
According to me, this error occurs because the deserializer expects URIs to fetch the answers from, instead of having the answers nested in the JSON. Adding the annotation tells it to look in JSON instead.
I'm still seeing this error with 2.3.0.M1, but I finally found a workaround.
The basic issue is this: If you post the url of the embedded entity in the JSON, it works. If you post the actual embedded entity JSON, it doesn't. It tries to deserialize the entity JSON into a URI, which of course fails.
It looks like the issue is with the two TypeConstrainedMappingJackson2HttpMessageConverter objects that spring data rest creates in its configuration (in RepositoryRestMvcConfiguration.defaultMessageConverters()).
I finally got around the issue by configuring the supported media types of the messageConverters so that it skips those two and hits the plain MappingJackson2HttpMessageConverter, which works fine with nested entities.
For example, if you extend RepositoryRestMvcConfiguration and add this method, then when you send a request with content-type of 'application/json', it will hit the plain MappingJackson2HttpMessageConverter instead of trying to deserialize into URIs:
#Override
public void configureHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
((MappingJackson2HttpMessageConverter) messageConverters.get(0))
.setSupportedMediaTypes(asList(MediaTypes.HAL_JSON));
((MappingJackson2HttpMessageConverter) messageConverters.get(2))
.setSupportedMediaTypes(asList(MediaType.APPLICATION_JSON));
}
That configures the message converters produced by defaultMessageConverters() in RepositoryRestMvcConfiguration.
Keep in mind that the plain objectMapper can't handle URIs in the JSON - you'll still need to hit one of the two preconfigured message converters any time you pass URIs of embedded entities.
One issue with your JSON is that you are trying to deserialize a string as a question:
"question": "http://localhost:8080/question/6"
In your Answer object, Jackson is expecting an object for question. It appears that you are using URLs for IDs, so instead of a string you need to pass something like this for your question:
"question": {
"id": "http://localhost:8080/question/6"
}
Try to update "Spring Boot Data REST Starter" library. Worked for me.
With Spring Boot 2.7.2 it is achievable with the following config (accepts both links and entities in the request bodies):
package com.my.project.config;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.CreatorProperty;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
import org.springframework.data.util.StreamUtils;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.ReflectionUtils;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static com.fasterxml.jackson.core.JsonToken.START_OBJECT;
// Allows POST'ing nested objects and not only links
#Configuration
public class CustomRepositoryRestMvcConfiguration implements RepositoryRestConfigurer {
private final ApplicationContext context;
private final PersistentEntities entities;
private final RepositoryInvokerFactory invokerFactory;
private final Repositories repositories;
private final Associations associations;
private final ExcerptProjector projector;
private final ObjectProvider<RepresentationModelProcessorInvoker> modelInvoker;
private final LinkCollector linkCollector;
private final RepositoryRestConfiguration repositoryRestConfiguration;
public CustomRepositoryRestMvcConfiguration(
ApplicationContext context,
PersistentEntities entities,
#Lazy RepositoryInvokerFactory invokerFactory,
Repositories repositories,
#Lazy Associations associations,
#Lazy ExcerptProjector projector,
#Lazy ObjectProvider<RepresentationModelProcessorInvoker> modelInvoker,
#Lazy LinkCollector linkCollector,
#Lazy RepositoryRestConfiguration repositoryRestConfiguration) {
this.context = context;
this.entities = entities;
this.invokerFactory = invokerFactory;
this.repositories = repositories;
this.associations = associations;
this.projector = projector;
this.modelInvoker = modelInvoker;
this.linkCollector = linkCollector;
this.repositoryRestConfiguration = repositoryRestConfiguration;
}
#Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(persistentEntityJackson2Module(linkCollector));
}
protected Module persistentEntityJackson2Module(LinkCollector linkCollector) {
List<EntityLookup<?>> lookups = new ArrayList<>();
lookups.addAll(repositoryRestConfiguration.getEntityLookups(repositories));
lookups.addAll((Collection) beansOfType(context, EntityLookup.class).get());
EmbeddedResourcesAssembler assembler = new EmbeddedResourcesAssembler(entities, associations, projector);
PersistentEntityJackson2Module.LookupObjectSerializer lookupObjectSerializer = new PersistentEntityJackson2Module.LookupObjectSerializer(PluginRegistry.of(lookups));
// AssociationUriResolvingDeserializerModifier delegates
return new NestedSupportPersistentEntityJackson2Module(associations,
entities,
new UriToEntityConverter(entities, invokerFactory, repositories),
linkCollector,
invokerFactory,
lookupObjectSerializer,
modelInvoker.getObject(),
assembler
);
}
public static class NestedSupportPersistentEntityJackson2Module extends PersistentEntityJackson2Module {
public NestedSupportPersistentEntityJackson2Module(Associations associations,
PersistentEntities entities,
UriToEntityConverter converter,
LinkCollector collector,
RepositoryInvokerFactory factory,
LookupObjectSerializer lookupObjectSerializer,
RepresentationModelProcessorInvoker invoker,
EmbeddedResourcesAssembler assembler) {
super(associations, entities, converter, collector, factory, lookupObjectSerializer, invoker, assembler);
}
#Override
public SimpleModule setDeserializerModifier(BeanDeserializerModifier mod) {
super.setDeserializerModifier(new NestedObjectSuppAssociationUriResolvingDeserializerModifier(
(PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier) mod)
);
return this;
}
}
#RequiredArgsConstructor
public static class NestedObjectSuppAssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier {
private final PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier uriDelegate;
#SneakyThrows
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config,
BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
// Pushes Uri* deserializer
uriDelegate.updateBuilder(config, beanDesc, builder);
// Replace Uri* deserializers with delegates
var customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config);
var properties = builder.getProperties();
while (properties.hasNext()) {
var prop = properties.next();
if (!prop.hasValueDeserializer()) {
continue;
}
if (prop.getValueDeserializer() instanceof PersistentEntityJackson2Module.UriStringDeserializer) {
customizer.replacePropertyIfNeeded(
builder,
prop.withValueDeserializer(new ObjectOrUriStringDeserializer(
prop.getValueDeserializer().handledType(),
prop.getValueDeserializer(),
new LateDelegatingDeser(prop.getType())
))
);
}
if ((Object) prop.getValueDeserializer() instanceof CollectionDeserializer) {
var collDeser = (CollectionDeserializer) ((Object) prop.getValueDeserializer());
if (!(collDeser.getContentDeserializer() instanceof PersistentEntityJackson2Module.UriStringDeserializer)) {
continue;
}
customizer.replacePropertyIfNeeded(
builder,
prop.withValueDeserializer(
new CollectionDeserializer(
collDeser.getValueType(),
new ObjectOrUriStringDeserializer(
prop.getValueDeserializer().handledType(),
((CollectionDeserializer) (Object) prop.getValueDeserializer()).getContentDeserializer(),
new LateDelegatingDeser(prop.getType().getContentType())
),
null,
collDeser.getValueInstantiator()
)
)
);
}
}
return customizer.conclude(builder);
}
#Getter
#RequiredArgsConstructor
public static class LateDelegatingDeser extends JsonDeserializer<Object> {
private final JavaType type;
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return ctxt.findNonContextualValueDeserializer(type).deserialize(p, ctxt);
}
}
}
public static class ObjectOrUriStringDeserializer extends StdDeserializer<Object> {
private final JsonDeserializer<Object> uriDelegate;
private final JsonDeserializer<Object> vanillaDelegate;
public ObjectOrUriStringDeserializer(Class<?> type, JsonDeserializer<Object> uriDelegate, JsonDeserializer<Object> vanillaDelegate) {
super(type);
this.uriDelegate = uriDelegate;
this.vanillaDelegate = vanillaDelegate;
}
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JacksonException {
if (START_OBJECT == jp.getCurrentToken()) {
return vanillaDelegate.deserialize(jp, ctxt);
}
return uriDelegate.deserialize(jp, ctxt);
}
}
// Copied from original ValueInstantiatorCustomizer
public static class ValueInstantiatorCustomizer {
private final SettableBeanProperty[] properties;
private final StdValueInstantiator instantiator;
ValueInstantiatorCustomizer(ValueInstantiator instantiator, DeserializationConfig config) {
this.instantiator = StdValueInstantiator.class.isInstance(instantiator) //
? StdValueInstantiator.class.cast(instantiator) //
: null;
this.properties = this.instantiator == null || this.instantiator.getFromObjectArguments(config) == null //
? new SettableBeanProperty[0] //
: this.instantiator.getFromObjectArguments(config).clone(); //
}
/**
* Replaces the logically same property with the given {#link SettableBeanProperty} on the given
* {#link BeanDeserializerBuilder}. In case we get a {#link CreatorProperty} we also register that one to be later
* exposed via the {#link ValueInstantiator} backing the {#link BeanDeserializerBuilder}.
*
* #param builder must not be {#literal null}.
* #param property must not be {#literal null}.
*/
void replacePropertyIfNeeded(BeanDeserializerBuilder builder, SettableBeanProperty property) {
builder.addOrReplaceProperty(property, false);
if (!CreatorProperty.class.isInstance(property)) {
return;
}
properties[((CreatorProperty) property).getCreatorIndex()] = property;
}
/**
* Concludes the setup of the given {#link BeanDeserializerBuilder} by reflectively registering the potentially
* customized {#link SettableBeanProperty} instances in the {#link ValueInstantiator} backing the builder.
*
* #param builder must not be {#literal null}.
* #return
*/
BeanDeserializerBuilder conclude(BeanDeserializerBuilder builder) {
if (instantiator == null) {
return builder;
}
Field field = ReflectionUtils.findField(StdValueInstantiator.class, "_constructorArguments");
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, instantiator, properties);
builder.setValueInstantiator(instantiator);
return builder;
}
}
private static <S> org.springframework.data.util.Lazy<List<S>> beansOfType(ApplicationContext context, Class<?> type) {
return org.springframework.data.util.Lazy.of(() -> (List<S>) context.getBeanProvider(type)
.orderedStream()
.collect(StreamUtils.toUnmodifiableList()));
}
}
It is ugly, but it works. Don't forget about cascades and proper setters for entities, i.e. one must have for OneToMany:
public class DeliveryOrder {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private Collection<Delivery> deliveries;
public void setDeliveries(Collection<Delivery> deliveries) {
if (null != deliveries) {
deliveries.forEach(delivery -> delivery.setOrder(this));
}
this.deliveries = deliveries;
}
}