How to POST nested entities with Spring Data REST

17,904

Solution 1

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.

Solution 2

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.

Solution 3

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"
}
Share:
17,904
73nko
Author by

73nko

Updated on June 04, 2022

Comments

  • 73nko
    73nko almost 2 years

    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*
    
    • Michał Ziober
      Michał Ziober almost 10 years
      Could you also show your controller method, stacktrace and POJO class?
    • 73nko
      73nko almost 10 years
      I've edit the main post with all the information that you request. Thanks for your help.
    • Michał Ziober
      Michał Ziober almost 10 years
      Maybe a problem is in wrong data? Is it ok: "question": "http://localhost:8080/question/6". Maybe it should look like this: "question": 6. Why are you sending URLs instead of ids?
    • 73nko
      73nko almost 10 years
      Because there is a relationship between questions and answers, these part is ok, I've tried it Individualy and works without problem. The issue is when I nest and object inside an other in a list. For example, if I do this post first only the inspection and one post for each answer and another for each reply is working but I thing is absurd when I have a questionary with more than 350 answers an each of them have 3 or 4 replys.... It have to be a way to do it in one nested object...
    • Sam Berry
      Sam Berry almost 10 years
      Your setter for answers takes a list of Respuesta objects instead of Answer objects, is this a typo or purposeful?
    • 73nko
      73nko almost 10 years
      That's because I've got the app in Spanish and I've translated all for the post and I didn't see that. Respuesta is the same of Answer ;).
    • brwngrldev
      brwngrldev almost 10 years
      Did you ever figure this out?
    • JBCP
      JBCP over 9 years
      I believe this page describes the right way to get this working, but when i try the method there, i get the same error you do: github.com/spring-projects/spring-data-rest/wiki/…
    • InsaurraldeAP
      InsaurraldeAP over 8 years
      Same here, did you solve this?
  • 73nko
    73nko almost 10 years
    Sam, thanks for your answer but It's still not working. I've got the same error. Any other idea?
  • Jason
    Jason about 9 years
    Do you mean that you can't POST an object that has one field as an embedded entity and another field as a URI?
  • TLA
    TLA about 9 years
    Yes, until this is fixed that won't work. If you override configureHttpMessageConverters like I suggested, you'll still need to pick one strategy per request - either embed all nested entities as JSON or send URIs for all of them - and set the content-type of the request appropriately so that it hits the correct messageConverter.
  • Jason
    Jason about 9 years
    Do you happen to know if there is a bug report for this?
  • verystrongjoe
    verystrongjoe over 8 years
    I am using spring-boot-starter-data-rest-1.2.5.RELEASE.jar. But I still have a problem. Which version do I have to upgrade?
  • user1523177
    user1523177 over 8 years
    This solution worked perfectly for me. After this change, I was able to add non-persisted items to a collection of a one-to-many resources.
  • phil
    phil about 8 years
    But now you can't use the CRUD-methods of your Questionary-repository, because they won't be exposed. Question creation will always handled by the Answer-repository, because on the Questionary-repository it will be not allowed. Is that right?
  • Jacob Barnes
    Jacob Barnes over 5 years
    Not sure if this will help anyone, but in the implementation I'm working with @RestResource(exported = false) needed to be added to the repository file, e.g. QuestionaryController, QuestionaryModel, QuestionaryRepo. I'm not sure if that is standard in spring or not, I've only worked with it a couple months for a basic backend service, but thought it might help someone in a similar situation.