Jackson java.util.Optional serialization does not include type ID - spring

I got the following classes:
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "oid"
)
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "clazz")
#JsonSubTypes({
#JsonSubTypes.Type(value = MySubEntity.class, name = "MySubEntity"),
})
public abstract class Entity {
...
}
public class MySubEntity extends Entity {
...
}
Now when I serialize that MySubEntity wrapped in an Optional then JSON does not contain the clazz attribute containing the type ID. Bug? When I serialize to List<MySubEntity> or just to MySubEntity it works fine.
Setup: jackson-databind 2.9.4, jackson-datatype-jdk8 2.9.4, serialization is done in Spring Boot application providing a RESTful web service.
EDIT: Here is the Spring REST method that returns the Optional:
#RequestMapping(method = RequestMethod.GET, value = "/{uuid}", produces = "application/json")
public Optional<MySubEntity> findByUuid(#PathVariable("uuid") String uuid) {
...
}
EDIT:
I made a SSCCE with a simple Spring REST controller and two tests. The first test is using ObjectMapper directly which is successful in deserialization although the clazz is missing. The second test calls the REST controller and fails with an error because clazz is missing:
Error while extracting response for type [class com.example.demo.MySubEntity] and content type [application/json;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Missing type id when trying to resolve subtype of [simple type, class com.example.demo.MySubEntity]: missing type id property 'clazz'; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class com.example.demo.MySubEntity]: missing type id property 'clazz'

This, indeed, looks like a bug. There is one workaround that I can suggest for this case, is to use JsonTypeInfo.As.EXISTING_PROPERTY and add field clazz to your Entity. There only one case with this approach is that the clazz must be set in java code manually. However this is easy to overcome.
Here is the full code for suggested workaround:
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "oid"
)
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY, //field must be present in the POJO
property = "clazz")
#JsonSubTypes({
#JsonSubTypes.Type(value = MySubEntity.class, name = "MySubEntity"),
})
public abstract class Entity {
#JsonProperty
private String uuid;
//Here we have to initialize this field manually.
//Here is the simple workaround to initialize in automatically
#JsonProperty
private String clazz = this.getClass().getSimpleName();
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getClazz() {
return clazz;
}
public void setClazz(String clazz) {
this.clazz = clazz;
}
}

Related

Facing issue while deserialization of JSON to object in spring boot

I am facing issue while deserialization the JSON to object in my springboot application.
I am having code as below :-
public interface IStatus{
}
#JsonTypeName("internal")
public enum InternalStatus implements IStatus{
INTERNAL_1,
INTERNAL_2
}
#JsonTypeName("external")
public enum ExternalStatus implements IStatus{
EXTERNAL_1,
EXTERNAL_2
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = As.PROPERTY,
property = "status")
#JsonSubTypes({
#JsonSubTypes.Type(value = InternalStatus.class, name = "internal"),
#JsonSubTypes.Type(value = ExternalStatus.class, name = "external")
})
#Setter
#Getter
public class Response{
IStatus status;
}
and my json corresponding to Response class is {status:"EXTERNAL_1"}
error details as below:-
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class IStatus]: missing type id property 'status'

SqlResultSetMapping to POJO class from a NamedNativeQuery throwing 'could not locate appropriate constructor'

I made a #NamedNativeQuery and attached it to an entity 'Doctor', on the same entity I attached a #SqlResultSetMapping which takes the columns of the query's result and maps them to a constructor of a specifically made POJO class. This query is also connected to a JPA method, which resides in the repository of the same entity.
However I keep getting an error that the appropriate constructor could not be located, as if the #SqlResultSetMapping or the POJO constructors are not in sync. (stack trace is at the bottom)
My entity, #NamedNativeQuery and #SqlResultSetMapping:
I tried the query directly on the DB and it gave the expected result, so I am just writing the select clause
#Entity
#NamedNativeQuery(
name =
"Doctor.findFreeExaminationTimes", // name of the JPA method in entity's repository (definition below)
query =
"SELECT on_date AS onDate, LAG(to_time, 1, '00:00') OVER mojWindow AS fromTime, from_time AS toTime " +
"...",
resultSetMapping = "freeTimesByDoctorId" // name of the mapping below
)
#SqlResultSetMapping(
name = "freeTimesByDoctorId", // result set mapping name
classes = #ConstructorResult(
targetClass = DoctorAvailabilityResponse.class, // my POJO class (definition below)
columns = { // order and types are the same as in the select clause above and the POJO constructor below
#ColumnResult(name = "onDate", type = java.sql.Date.class),
#ColumnResult(name = "fromTime", type = java.sql.Time.class),
#ColumnResult(name = "toTime",type = java.sql.Time.class)
}
)
)
public class Doctor extends User {...}
The POJO class which I mention in the #ConstructorResult under 'targetClass' has a constructor with the exact order, number and type, of arguments, specified under 'columns'
My POJO class which should be mapped to the query's result:
public class DoctorAvailabilityResponse {
final private java.sql.Date onDate;
final private java.sql.Time fromTime;
final private java.sql.Time toTime;
public DoctorAvailabilityResponse(java.sql.Date onDate, java.sql.Time fromTime, java.sql.Time toTime) {
this.onDate = onDate;
this.fromTime = fromTime;
this.toTime = toTime;
}
// getters
}
My repository:
#RepositoryRestResource
public interface DoctorRepository extends UserRepository<Doctor> {
// JPA method mapped to the named native query above
List<DoctorAvailabilityResponse> findFreeExaminationTimes(#Param("doctorId") Long doctorId);
}
However when testing this JPA method I get an exception with the message 'could not locate appropriate constructor'.
My test:
#SpringBootTest
public class DoctorTests {
#Autowired
private DoctorRepository doctorRepository;
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
#Test
public void shouldReturnDoctorAvailability() {
// Exception thrown here
List<DoctorAvailabilityResponse> freeTimes = doctorRepository.findFreeExaminationTimes(4L);
LOGGER.info(freeTimes.toString());
}
}
I cannot understand why is this happening. Is there a way to manually map this result set to the POJO while maintaining the JPA repository method?
Stack trace:
org.springframework.dao.InvalidDataAccessApiUsageException: Could not locate appropriate constructor on class : com.example.isaproj.isa_projekat_2019.Model.DTO.DoctorAvailabilityResponse; nested exception is java.lang.IllegalArgumentException: Could not locate appropriate constructor on class : com.example.isaproj.isa_projekat_2019.Model.DTO.DoctorAvailabilityResponse
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:374)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:256)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528)
...
...
Caused by: java.lang.IllegalArgumentException: Could not locate appropriate constructor on class : com.example.isaproj.isa_projekat_2019.Model.DTO.DoctorAvailabilityResponse
at org.hibernate.loader.custom.ConstructorResultColumnProcessor.resolveConstructor(ConstructorResultColumnProcessor.java:92)
at org.hibernate.loader.custom.ConstructorResultColumnProcessor.performDiscovery(ConstructorResultColumnProcessor.java:45)
at org.hibernate.loader.custom.CustomLoader.autoDiscoverTypes(CustomLoader.java:494)
at org.hibernate.loader.Loader.processResultSet(Loader.java:2333)
at org.hibernate.loader.Loader.getResultSet(Loader.java:2289)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2045)
at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:2007)
at org.hibernate.loader.Loader.doQuery(Loader.java:953)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:354)
at org.hibernate.loader.Loader.doList(Loader.java:2810)
at org.hibernate.loader.Loader.doList(Loader.java:2792)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2624)
at org.hibernate.loader.Loader.list(Loader.java:2619)
at org.hibernate.loader.custom.CustomLoader.list(CustomLoader.java:338)
at org.hibernate.internal.SessionImpl.listCustomQuery(SessionImpl.java:2137)
at org.hibernate.internal.AbstractSharedSessionContract.list(AbstractSharedSessionContract.java:1134)
at org.hibernate.query.internal.NativeQueryImpl.doList(NativeQueryImpl.java:173)
at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1526)
at org.hibernate.query.Query.getResultList(Query.java:165)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:409)
at com.sun.proxy.$Proxy212.getResultList(Unknown Source)
at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:126)
at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:154)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:142)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:618)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:605)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:353)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
... 73 more
Sanity check and alternative approach
To make a sanity check I deleted the #SqlResultSetMapping, in which case the query is supposed to return a list of 'Object[]' values, and then I tested each individual value in that array to check its type, it showed me that the types are what I assumed them to be 'java.sql.Date' and 'java.sql.Time' twice, and all three of them were in the expected order, (Date, Time, Time), which matches the order of the constructor parameters of my POJO class.
My entity and namedNativeQuery:
#Entity
#NamedNativeQuery(
name =
"Doctor.findFreeExaminationTimes",
query =
"SELECT on_date AS onDate, LAG(to_time, 1, '00:00') OVER mojWindow AS fromTime, from_time AS toTime " +
"..."
)
public class Doctor extends User {...}
My repository with a new return type:
#RepositoryRestResource
public interface DoctorRepository extends UserRepository<Doctor> {
List<Object[]> findFreeExaminationTimes(#Param("doctorId") Long doctorId);
}
My test:
#SpringBootTest
public class DoctorTests {
#Autowired
private DoctorRepository doctorRepository;
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
#Test
public void shouldReturnDoctorAvailability() {
// Exception thrown here
List<DoctorAvailabilityResponse> freeTimes = doctorRepository.findFreeExaminationTimes(4L);
freeTimes.stream().forEach((ft) -> {
// Values are in expected order and of expected types
String classNameOnDate = ft[0].getClass().toString(); // java.sql.Date
String classNameFromTime = ft[1].getClass().toString(); // java.sql.Time
String classNameToTime = ft[1].getClass().toString(); // java.sql.Time
// I suppose the mapping mechanism is supposed to do something like this, but fails for some reason
DoctorAvailabilityResponse dar = new DoctorAvailabilityResponse((Date)ft[0], (Time)ft[1], (Time)ft[2]);
});
LOGGER.info(freeTimes.toString());
}
}
Running this test works perfectly which supposedly shows that the problem is in the #SqlResultSetMapping or in POJO class.
I would appreciate any feedback. Thanks!
SOLUTION
I had to change the types in the #SqlResultSetMapping and in the constructor of my POJO class.
Changed #SqlResultSetMapping
#SqlResultSetMapping(
name = "freeTimesByDoctorId",
classes = #ConstructorResult(
targetClass = DoctorAvailabilityResponse.class,
columns = {
#ColumnResult(name = "onDate", type = String.class),
#ColumnResult(name = "fromTime", type = String.class),
#ColumnResult(name = "toTime",type = String.class)
}
)
)
Changed POJO class constructor
public DoctorAvailabilityResponse(String onDate, String fromTime, String toTime) {
this.onDate = Date.valueOf(onDate);
this.fromTime = Time.valueOf(fromTime);
this.toTime = Time.valueOf(toTime);
}
This alone did not solve my problem however as I got a hibernate exception as mentioned and solved in this SO question. According to this answer I also changed my repository and added an additional annotation.
Changed repository
#RepositoryRestResource
public interface DoctorRepository extends UserRepository<Doctor> {
#Query(nativeQuery = true) // This is added
List<DoctorAvailabilityResponse> findFreeExaminationTimes(#Param("doctorId") Long doctorId);
}
Now everything works, though the question remains why didn't #SqlResultSetMapping map java.sql.* types to the constructor in the first place.
#ConstructorResult does not work very well with java.sql.Date.class or java.sql.Time.class type. A way to fix your issue is to use String.class instead and then convert the String values to Date/Time in DoctorAvailabilityResponse's constructor

Spring Data Rest PATCH requests do not fill transient properties not backed by a field

I want to implement a PATCH-Requese on an entity 'User' for changing the password with an additional transient property 'oldpassword' to compare it in the EventHandler.
The POST- and the PUT-request fill the property.
The PATCH-request doesn't: 'oldpassword' is null.
I'm using
spring-boot-starter-parent
spring-boot-starter-data-rest (2.1.6)
spring-boot-starter-web (2.1.6)
spring-boot-starter-data-jpa (2.1.6)
spring-data-jpa 2.1.9
spring-data-rest 3.1.9
spring-security 5.1.5 (presumably irrelevant)
I tried
the annotation #JsonProperty("oldpassword") (even though POST and PUT work).
the annotation #JsonDeserialize (JSON: #Transient field not seralizing)
to configure Jackson to disable the check for #Transient annotations (JPA Transient Annotation and JSON)
#JsonAutoDetect(fieldVisibility = Visibility.ANY) as class decorator
The simplified code is:
The entity 'User'
#Entity
public class User implements UserDetails, Serializable {
[...]
#NotNull
String password;
#Transient
String newpassword;
#Transient
String oldpassword;
public void setPassword(String password) {
this.newpassword = password;
}
public void setOldpassword(String oldpassword) {
this.oldpassword = oldpassword;
}
[...]
}
The Repository
#RepositoryRestResource(exported = true)
public interface UserRepository extends JpaRepository<User, Long> {
}
PATCH-Request (
HTTP Method = PATCH
Request URI = /api/users/2
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Authorization:"Basic aXJ0Z2VuZGFhczpFaW4gcGFzc3dvcmQ="]
Body = {
"username": "myusername",
"password": "mynewpassword",
"oldpassword": "theoldone"
}
The EventHandler
#Component
#RepositoryEventHandler(User.class)
public class UserEventHandler {
#HandleBeforeSave
public void printdata(User p) {
/* returns the new password*/
System.out.println("newpassword" + p.newpassword);
/* returns null (if it's a PATCH-request) */
System.out.println("oldpassword" + p.oldpassword);
/* returns the old persisted password */
System.out.println("password" + p.password);
}
}
The transient property 'newpassword' works, since I use the setter of the persisted property 'password'.
It seems you want to create a change-password feature. It won't work this way. Create a unique controller method for it.
It's not a standard REST request anyway.

Deserialize JsonManagedReference on many to many relations

Deserialize JsonManagedReference on many to many relations
Jackson Version: 2.9.7
Objects with a ManyToMany relationship which is managed by #JsonManagedReference/#JsonBackReference cannot be deserialized. This is easiest to show with an example.
public class JsonReferenceTest {
class Customer {
#JsonManagedReference("users")
public Collection<User> users = new ArrayList<>();
public String name = "company";
}
class User {
#JsonBackReference("users")
public Collection<Customer> customers = new ArrayList<>();
public String name = "user";
}
ObjectMapper objectMapper = new ObjectMapper();
#Test
public void testDeserialize() throws IOException {
String customer = "{\"name\":\"asdf\"}";
objectMapper.readValue(customer, Customer.class);
}
}
Running the following test results in:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'users': back reference type (java.util.Collection) not compatible with managed type (com.widewail.JsonReferenceTest$Customer)
at [Source: (String)"{"name":"asdf"}"; line: 1, column: 1]
Digging into BeanDeserializerBase it looks like when checking that the back reference type matches the reference type it is not checking the contained type of the collection on the back reference side.
As the documentation from JsonBackReference says you can't use it with a Collection.
Annotation used to indicate that associated property is part of
two-way linkage between fields; and that its role is "child" (or
"back") link. Value type of the property must be a bean: it can not
be a Collection, Map, Array or enumeration.
You can use JsonIdentityInfo as decribed here.

Spring Boot Controller endpoint and ModelAttribute deep access

I would like to know how to access a deep collection class attribute in a GET request. My endpoint maps my query strings through #ModelAttribute annotation:
Given that:
public class MyEntity
{
Set<Item> items;
Integer status;
// getters setters
}
public class Item
{
String name;
// getters setters
}
And my GET request: localhost/entities/?status=0&items[0].name=Garry
Produces bellow behavior?
#RequestMapping(path = "/entities", method = RequestMethod.GET)
public List<MyEntity> findBy(#ModelAttribute MyEntity entity) {
// entity.getItems() is empty and an error is thrown: "Property referenced in indexed property path 'items[0]' is neither an array nor a List nor a Map."
}
Should my "items" be an array, List or Map? If so, thereĀ“s alternatives to keep using as Set?
Looks like there is some problem with the Set<Item>.
If you want to use Set for the items collection you have to initialize it and add some items:
e.g. like this:
public class MyEntity {
private Integer status;
private Set<Item> items;
public MyEntity() {
this.status = 0;
this.items = new HashSet<>();
this.items.add(new Item());
this.items.add(new Item());
}
//getters setters
}
but then you will be able to set only the values of this 2 items:
This will work: http://localhost:8081/map?status=1&items[0].name=asd&items[1].name=aaa
This will not work: http://localhost:8081/map?status=1&items[0].name=asd&items[1].name=aaa&items[2].name=aaa
it will say: Invalid property 'items[2]' of bean class MyEntity.
However if you switch to List:
public class MyEntity {
private Integer status;
private List<Item> items;
}
both urls map without the need to initialize anything and for various number of items.
note that I didn't use #ModelAttribute, just set the class as paramter
#GetMapping("map")//GetMapping is just a shortcut for RequestMapping
public MyEntity map(MyEntity myEntity) {
return myEntity;
}
Offtopic
Mapping a complex object in Get request sounds like a code smell to me.
Usually Get methods are used to get/read data and the url parameters are used to specify the values that should be used to filter the data that has to be read.
if you want to insert or update some data use POST, PATCH or PUT and put the complex object that you want to insert/update in the request body as JSON(you can map that in the Spring Controller with #RequestBody).

Resources