Using Entity with OneToMany and HATEOAS RessourceAssembler leads to infinite recursion

Viewed 962

I'm using two JPA entities annotated with @OneToMany (parent) <-> @ManyToOne (child) and I also wrote a RessourceAssembler to turn the entities into resources in the controller of a Springboot application (see below for code samples).

Without the relationship @OneToMany in the parent entity, Ressource assembling and serialisation works just fine.

As soon as I add the OneToMany relation on the parent the serialisation breaks with this:

"Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.hateoas.Resource[\"content\"]->com.marcelser.app.entities.Storage[\"storageStock\"])"

As you can see the infinite loop comes from the hateoas Resource, not the entities themselves.

I already tried to add @JsonManagedReference and @JsonBackReference on the entities or @JsonIgnore on the child but nothing really helps. The Hateoas RessourceAssembler always ends up in a infinite loop as soon as the child entity is embedded. It seems that shose @Json.... annotations help with the JSON serialisation of the entity itself but they don't solve problems with the RessourceAssembler

I have these 2 entities (Storage & Stock)

@Entity
@Table(name = "storage")
@Data
public class Storage {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL,
            fetch = FetchType.LAZY,
            mappedBy = "storage")
    private Set<Stock> storageStock = new HashSet<>();;
}

@Entity
@Table(name = "stock")
@Data
public class Stock {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "storage_id")
    private Storage storage;

    ... other fields ommitted
}

and I'm using a RessourceAssemlber like follows for the parent entity 'Storage':

@Component
public class StorageResourceAssembler implements ResourceAssembler<Storage, Resource<Storage>>  {

    @Override
    public Resource<Storage> toResource(Storage storage) {

        return new Resource<>(storage,
                linkTo(methodOn(StorageController.class).one(storage.getId())).withSelfRel(),
                linkTo(methodOn(StorageController.class).all()).withRel("storages"));
    }
}

and in the controller I have 2 get classes to list all or just a single Storage with its childs

public class StorageController {
    private final StorageRepository repository;
    private final StorageResourceAssembler assembler;

        @GetMapping
    ResponseEntity<?>  all() {

        List<Resource<Storage>> storages = repository.findAll().stream()
                .map(assembler::toResource)
                .collect(Collectors.toList());

        Resources<Resource<Storage>> resources = new Resources<>(storages,
                linkTo(methodOn(StorageController.class).all()).withSelfRel());
        return ResponseEntity.ok(resources);
    }

    private static final Logger log = LoggerFactory.getLogger(StorageController.class);

    StorageController(StorageRepository repository, StorageResourceAssembler assembler) {
        this.repository = repository;
        this.assembler = assembler;
    }


    @GetMapping("/{id}")
    ResponseEntity<?> one(@PathVariable Long id) {
        try {
            Storage storage = repository.findById(id)
                    .orElseThrow(() -> new EntityNotFoundException(id));
            Resource<Storage> resource = assembler.toResource(storage);
            return ResponseEntity.ok(resource);
        }
        catch (EntityNotFoundException e) {
            log.info(e.getLocalizedMessage());
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body (new VndErrors.VndError("Storage not found", "could not find storage with id " + id ));
        }
    }

    ... omitted Put/Post/Delete

}

Can anyone enlighten me how I can solve this infinite loop in HateOAS. What I want is that the embedded child entries just either don't link back to the parent (so no links to parent are created) or they contain the link for the one level but no further processing is done.

2 Answers

To handle the problem related to the serialization of the model using Jackson API when the model attributes have a lazy loading defined, we have to tell the serializer to ignore the chain or helpful garbage that Hibernate adds to classes, so it can manage lazy loading of data by declaring @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) annotation.

@Entity
@Table(name = "storage")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Storage {...



@Entity
@Table(name = "stock")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Stock {...

or you can just declare unilaterally mapping commenting the Storage entity declaration and changing the private Storage storage; to fetch EAGER @ManyToOne(fetch = FetchType.EAGER) in Stock class.

@Entity
@Table(name = "storage")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Storage {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private String name;

    /*@OneToMany(cascade = CascadeType.ALL,
            fetch = FetchType.LAZY,
            mappedBy = "storage")
    private Set<Stock> storageStock = new HashSet<>();;*/
}

@Entity
@Table(name = "stock")
@Data
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Stock {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "storage_id")
    private Storage storage;

    ... other fields ommitted
}

Maybe a little late, but I've had this problem or very similar and I've only found one solution. The same error 500 gave me the clue on how to solve it:

Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.hateoas.PagedModel["_embedded"]->java.util.Collections$UnmodifiableMap["usuarios"]->java.util.ArrayList[0]->org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer$1["content"]->com.tfg.modelos.Usuario["rol"]->org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$PersistentEntityResourceSerializer$1["content"]->com.tfg.modelos.Rol$HibernateProxy$QFcQnzTB["hibernateLazyInitializer"])

So I only had to add in the application.properties:

spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false
Related