Reputation: 443
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*
Upvotes: 33
Views: 19494
Reputation: 301
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;
}
}
Upvotes: 0
Reputation: 37
Try to update "Spring Boot Data REST Starter" library. Worked for me.
Upvotes: 0
Reputation: 675
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.
Upvotes: 25
Reputation: 166
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.
Upvotes: 7
Reputation: 7844
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"
}
Upvotes: 4