Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preventing infinite recursion in JAX-RS on JPA Entity Serialization (JSON) (without using Jackson annotations)

I have an Entity as follows:

@XmlRootElement
@Entity
@Table(name="CATEGORY")
@Access(AccessType.FIELD)
@Cacheable
@NamedQueries({
    @NamedQuery(name="category.countAllDeleted", query="SELECT COUNT(c) FROM Category c WHERE c.deletionTimestamp IS NOT NULL"),
    @NamedQuery(name="category.findAllNonDeleted", query="SELECT c from Category c WHERE c.deletionTimestamp IS NULL"),
    @NamedQuery(name="category.findByCategoryName", query="SELECT c FROM Category c JOIN c.descriptions cd WHERE LOWER(TRIM(cd.name)) LIKE ?1")
})
public class Category extends AbstractSoftDeleteAuditableEntity<Integer> implements za.co.sindi.persistence.entity.Entity<Integer>, Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 4600301568861226295L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="CATEGORY_ID", nullable=false)
    private int id;

    @ManyToOne
    @JoinColumn(name="PARENT_CATEGORY_ID")
    private Category parent;

    @OneToMany(cascade= CascadeType.ALL, mappedBy="category")
    private List<CategoryDescription> descriptions;

    public void addDescription(CategoryDescription description) {
        if (description != null) {
            if (descriptions == null) {
                descriptions = new ArrayList<CategoryDescription>();
            }

            descriptions.add(description);
        }
    }

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#getId()
     */
    public Integer getId() {
        // TODO Auto-generated method stub
        return id;
    }

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#setId(java.io.Serializable)
     */
    public void setId(Integer id) {
        // TODO Auto-generated method stub
        this.id = (id == null) ? 0 : id;
    }

    /**
     * @return the parent
     */
    public Category getParent() {
        return parent;
    }

    /**
     * @param parent the parent to set
     */
    public void setParent(Category parent) {
        this.parent = parent;
    }

    /**
     * @return the descriptions
     */
    public List<CategoryDescription> getDescriptions() {
        return descriptions;
    }

    /**
     * @param descriptions the descriptions to set
     */
    public void setDescriptions(List<CategoryDescription> descriptions) {
        this.descriptions = descriptions;
    }
}

AND:

@XmlRootElement
@Entity
@Table(name="CATEGORY_DESCRIPTION")
@Access(AccessType.FIELD)
@Cacheable
public class CategoryDescription extends AbstractModifiableAuditableEntity<CategoryDescriptionKey> implements za.co.sindi.persistence.entity.Entity<CategoryDescriptionKey>, Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 4506134647012663247L;

    @EmbeddedId
    private CategoryDescriptionKey id;

    @MapsId("categoryId")
    @ManyToOne/*(fetch=FetchType.LAZY)*/
    @JoinColumn(name="CATEGORY_ID", insertable=false, updatable=false, nullable=false)
    private Category category;

    @MapsId("languageCode")
    @ManyToOne/*(fetch=FetchType.LAZY)*/
    @JoinColumn(name="LANGUAGE_CODE", insertable=false, updatable=false, nullable=false)
    private Language language;

    @Column(name="CATEGORY_NAME", nullable=false)
    private String name;

    @Column(name="DESCRIPTION_PLAINTEXT", nullable=false)
    private String descriptionPlainText;

    @Column(name="DESCRIPTION_MARKDOWN", nullable=false)
    private String descriptionMarkdown;

    @Column(name="DESCRIPTION_HTML", nullable=false)
    private String descriptionHtml;

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#getId()
     */
    public CategoryDescriptionKey getId() {
        // TODO Auto-generated method stub
        return id;
    }

    /* (non-Javadoc)
     * @see za.co.sindi.entity.IDBasedEntity#setId(java.io.Serializable)
     */
    public void setId(CategoryDescriptionKey id) {
        // TODO Auto-generated method stub
        this.id = id;
    }

    /**
     * @return the category
     */
    public Category getCategory() {
        return category;
    }

    /**
     * @param category the category to set
     */
    public void setCategory(Category category) {
        this.category = category;
    }

    /**
     * @return the language
     */
    public Language getLanguage() {
        return language;
    }

    /**
     * @param language the language to set
     */
    public void setLanguage(Language language) {
        this.language = language;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the descriptionPlainText
     */
    public String getDescriptionPlainText() {
        return descriptionPlainText;
    }

    /**
     * @param descriptionPlainText the descriptionPlainText to set
     */
    public void setDescriptionPlainText(String descriptionPlainText) {
        this.descriptionPlainText = descriptionPlainText;
    }

    /**
     * @return the descriptionMarkdown
     */
    public String getDescriptionMarkdown() {
        return descriptionMarkdown;
    }

    /**
     * @param descriptionMarkdown the descriptionMarkdown to set
     */
    public void setDescriptionMarkdown(String descriptionMarkdown) {
        this.descriptionMarkdown = descriptionMarkdown;
    }

    /**
     * @return the descriptionHtml
     */
    public String getDescriptionHtml() {
        return descriptionHtml;
    }

    /**
     * @param descriptionHtml the descriptionHtml to set
     */
    public void setDescriptionHtml(String descriptionHtml) {
        this.descriptionHtml = descriptionHtml;
    }   
}

When returning a Collection<Category> using JAX-RS and deploying on JBoss Wildfly 8.2.0-Final, I get the following stacktrace:

Caused by: com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: za.co.sindi.unsteve.persistence.entity.Category["descriptions"]->org.hibernate.collection.internal.PersistentBag[0]->za.co.sindi.unsteve.persistence.entity.CategoryDescription["category"]->za.co.sindi.unsteve.persistence.entity.Category["descriptions"]->

There are answers answered in questions like this question, that requires to use Jackson specific annotations. My project's requirement is to strictly stick to Java EE specific frameworks. Is there a solution that we can use to prevent infinite recursion without using Jackson annotations? If not, can we create a config file (XML file, etc.), that Jackson can use in place of annotations? The reason for this is that the application must not only be bound to Wildfly specific libraries.

like image 577
Buhake Sindi Avatar asked Jun 06 '15 14:06

Buhake Sindi


2 Answers

I'd say you have few options here:

  1. Use transient keyword or @XmlTransient to let JAX-RS ignore some properties / fields (they won't be marshalled),
  2. Use DTO to better reflect your data structure for the end-user; in time the differences between your entity, how it's modelled in the RDBMS and what you're returning to the user will be larger and larger,
  3. Use combination of the two above options and mark some fields as transient and at the same time provide other "JAX-RS friendly" accessor, e.g. return only the ID of the Category instead of the whole object.

There are also some Jackson specific solutions beside the @JsonIgnore like:

  • Jackson views -- @JsonView can be used to achieve the same way in even more flexible way (e.g. it allows you to define when you want to return simplified object with no dependencies (just the ID of the related object) and when to return whole object; you specify what view to use e.g. on the JAX-RS entrypoint,
  • Object Identity that will recognize circular dependencies when marshalling the object and prevent infinite recursion (first hit of the object means putting it as a whole as-is, every other hit of the same object means putting only its ID).

I'm sure that there are also other solutions, but taking about the above ones, I would personally go with the DTO in a long-run. You can use some automatic mapping solutions like Dozer to help you with this nasty repetition job.
That being said, it's good to keep the data you present to and accept from the user separate from your internals.

like image 160
Piotr Nowicki Avatar answered Sep 28 '22 04:09

Piotr Nowicki


Yes. Create a dedicated data structure (e.g. a Data Transfer Object or DTO) and map the fields you want to send from your HTTP endpoint.

You're mixing concerns and this usually ends up badly.

JPA entities are your API towards your data structure, REST representations (JSON or XML DTOs) are the data payload provided by your REST API.

like image 39
mp911de Avatar answered Sep 28 '22 04:09

mp911de