Parent-child relation between two objects causes JSON StackOverflowError - java

I am trying to achieve a parent-child relation between some objects and I ran into a bit of trouble.
In my case, I am trying to store objects within other objects (e.g. container stores multiple items or other containers with items). The tricky part is that every object in the storage should be able to tell what it's outermost parent object is. While this seems to work in my in-memory database (using h2 at the moment), trying to get a JSON representation of all my storage items gives this (I return a List<StorageUnit> ):
Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: java.util.ArrayList[0]->com.warehousing.storage.FixedContentsCase["contents"]->java.util.ArrayList[0]->com.warehousing.storage.FixedContentsCase["contents"]->...
Here are the classes:
StorageUnit
#Entity
#Inheritance
public abstract class StorageUnit {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne
private Location location;
protected Long parentContainerId;
// <getters & setters>
public abstract List<StorageUnit> getContents();
}
FixedContentCase
#Entity
public class FixedContentsCase extends StorageUnit {
#OneToMany
private List<Item> items;
public FixedContentsCase() {
super();
items = new ArrayList<>();
}
// <getters & setters>
#Override
public List<StorageUnit> getContents() {
// Return the case itself and its contents
List<StorageUnit> contents = new ArrayList<>(Arrays.asList(this));
for (StorageUnit item : items)
contents.addAll(item.getContents());
return contents;
}
}
Item
#Entity
public class Item extends StorageUnit {
private String description;
public Item() {
super();
this.description = "";
}
// <getters & setters>
#Override
public List<StorageUnit> getContents() {
return Arrays.asList(this);
}
}
I have tried to annotate the StorageUnit class with #JsonIgnoreProperties("parentContainerId") but it didn't work. Annotating parentContainerId with #JsonIgnore didn't help either. I also tried annotating the getters instead of the attributes themselves (as per following). Is there a way to work around this or is some kind of design change necessary? Thanks!

Using Jackson this is definitely possible by annotations like #JsonIgnore or the DTO approach BugsForBreakfast mentioned.
I created a jackson MixIn handler to allow dynamic filtering which i use to avoid the boilerplate of DTOs
https://github.com/Antibrumm/jackson-antpathfilter
The examples in the readme should show how it works and if it‘s a possible solution for you.

Your problem is that you add the storage unit itself to its list of contents, leading to infinite recursion if you traverse the tree downwards. The solution: Use a reference and only serialize the object once, using #JsonIdentityInfo and #JsonIdentityReference:
public class MyTest {
#Test
public void myTest() throws JsonProcessingException {
final FixedContentsCase fcc = new FixedContentsCase();
fcc.setId(Long.valueOf(1));
final Item item = new Item();
item.setId(Long.valueOf(2));
item.setDescription("item 1");
fcc.getItems().add(item);
final ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(fcc));
}
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonIdentityReference(alwaysAsId = false)
class Item extends StorageUnit {
...
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonIdentityReference(alwaysAsId = false)
class FixedContentsCase extends StorageUnit {
...
}
abstract class StorageUnit {
...
}

Related

Automatically convert Spring JPA Entity OneToMany to List<ID> in DTO and back (ModelMapper)

Animal.java
#Data
#Entity
public class Animal implements MyEntityInterface {
public enum Sex {MALE, FEMALE}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private Sex sex;
private boolean castrated;
#OneToMany
private List<Symptom> symptoms;
}
AnimalDTO.java
#Getter
#Setter
public class AnimalDTO implements Serializable {
private long id;
private String name;
private Animal.Sex sex;
private boolean castrated;
private List<Long> symptoms;
}
I wish for a list of Symptoms to be automatically mapped to a list of ID's. This could be achieved in many ways, such as creating a TypeMap, creating a Converter or even just by creating a method in AnimalDTO.java:
public void setSymptoms(List<Symptom> symptoms) {
if (symptoms != null)
this.symptoms = symptoms.stream().map(s -> s.getId()).collect(Collectors.toList());
}
But now imagine it's not only Symptoms, but 50 other fields too. That's a lot of code for the same functionality. And then, it's not only Animal to AnimalDTO, but another 30 different classes with their respective DTOs too.
Also, that still leaves the way back open. From ID to entity. This can (in theory) be achieved easily with the following pseudocode:
List<EntityMemberField.class> list;
for (var entityid : listOfEntityIDsOfDto) {
Object persistedObject = entityManager.find(EntityMemberField.class, entityid);
list.add(persistedObject);
}
...
ModelMapperDestination.setField(list);
This is the same for absolutely every Entity/DTO and should automatically happen for every Entity relationship where the Entity implements MyEntityInterface.
An idea how I could achieve that would be overriding MappingEngineImpl.java from ModelMapper which I register as a Spring Service and inject the EntityManager into, but how could I get ModelMapper to use mine? Or is there maybe an easier way?
The goal is to have a fairly automated conversion from Spring Entities to their corresponding DTO by... just calling modelMapper.map(entity, EntityDTO.class);

convertToDatabaseColumn when data is not being persisted?

I have implemented method AttributeConverter.convertToEntityAttribute to load json data from the db. I am not trying to persist data, but for some reason convertToDatabaseColumn is being called.
This is what happens:
1. I call a repository method
2. then a call to AttributeConverter.convertToEntityAttribute follows -> returns a list of entity Cx. Till this point everything is normal.
3. But for some reason AttributeConverter.convertToDatabaseColumn is called right after, with that same list of entity Cx as argument -> returns stringV
4.Now convertToEntityAttribute is called again with stringV as argument, which is also strange.
Could it be that a #OneToOne relation is causing this? Why is this executing convertToDatabaseColumn if I'm not persisting an entity, at least explicitly?
All of this happens just by calling a single method in one of my repository classes:
Here is the code
public interface RSTRepository extends CrudRepository<RST, Long> {
List<RST> findByDuctNameIgnoreCase(String ductName);
}
#Entity
#Table(name="r_s_t")
public class RST {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#OneToOne
#JoinColumn(name = "r_s_id")
private Rs rs;
#Column(name = "channel")
private String channelName;
...
}
#Entity
#Table(name="r_s")
public class RS {
#Id
#Column(name = "rs_id", columnDefinition = "json")
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column(name = "c_x", columnDefinition = "json")
#Convert(converter = JsonToCxConverter.class)
private List<Cx> cxs;
...
}
public class Cx {
private Long someId;
private List<Long> values;
...
}
#Converter
public class JsonToCxConverterimplements AttributeConverter<List<Cx>, String>{
//this gets executed
#Override
public String convertToDatabaseColumn(List<Cx> entityAttribute) {
ObjectMapper objectMapper = new ObjectMapper();
log.info("--------------------");
return "";
}
#Override
public List<Cs> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isEmpty()) return Collections.emptyList();
//... uses the object mapper to parse the json and return a simple object.
...
}
Like I said, this happens when calling RSTRepository.findByDuctNameIgnoreCase
Yes its really behaving like you are saying. Also when persisting RST, Converter is also called 3x.
It also called 3x when reading just RS entity, i.e. it is not caused by #OneToOne relation.
I think it is how hibernate works. It should not be a problem, you get the right data without error.
From stacktrace I see that second and third call is from AbstractRowReader.performTwoPhaseLoad().
at org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.performTwoPhaseLoad(AbstractRowReader.java:241)
at org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishUp(AbstractRowReader.java:209)
at org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl.extractResults(ResultSetProcessorImpl.java:133)
I think its something that cannot be disabled. From hibernate sources I see that entity is registered to "hydrating". I found more about it here https://stackoverflow.com/a/29538797/2044957
Another thing: This is happening only when using converter on a collection. Converter is called once if it used on single type, for example AttributeConverter<String, String>.

How to globally apply #JsonIgnoreProperties(value = { "id" }) for all POJO classes of application (jackson api)

I have multiple classes and for all of them I don't want id field to be the part of my output JSON string (serialization). Let say I have 2 classes
#JsonIgnoreProperties(value = { "id" })
public final class Person {
private ObjectId id;
//........
}
#JsonIgnoreProperties(value = { "id" })
public final class Address{
private ObjectId id;
//........
}
Now I don't want to specify #JsonIgnoreProperties(value = { "id" }) manually on all my 1000 classes. Is there any global way to do, so that I can apply this part for all my classes? Very similarly like mapper.setSerializationInclusion(Include.NON_NULL) in below method?
public String serialize(T dataObject) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL);
String result = mapper.writeValueAsString(dataObject);
return result;
}
One way I tried is to make a super class and applied #JsonIgnoreProperties on top of that (which works). But still I have to write "extends" in every child class which I don't prefer. Is there any way in which I can apply this setting without adding anything additional in my pojo class?
There might be more efficient solution but you might override default marshaller with the one which excludes unwanted fields but keeps all others.

Use #XmlTransient only in some cases

I have two classes:
public class A implements Serializable {
...
#OneToMany(cascade = CascadeType.ALL, mappedBy = "fieldID")
private Collection<B> bCollection;
...
public Collection<B> getBCollection()
{
return bCollection;
}
public void setBCollection(Collection<B> bCollection)
{
this.bCollection = bCollection;
}
}
public class B implements Serializable {
...
#JoinColumn(name = "aID", referencedColumnName = "id")
#ManyToOne(optional = false)
private A aID;
...
#XmlTransient
public A getAID() {
return aID;
}
public void setAID(A aID) {
this.aID= aID;
}
}
I was always using A class - it is working as inteded, but now, I want to use B class in RESTful GET method. However, when I try to do that, #XmlTransient prevents showing A field. Is it possible to use #XmlTransient on A class, when I am using B class and to use it on B class, when I am using A class?
One easy solution is to include https://eclipse.org/eclipselink/moxy.php and start using #XmlInverseReference annotation for bi-directional dependencies. http://eclipse.org/eclipselink/api/2.2/org/eclipse/persistence/oxm/annotations/XmlInverseReference.html.
If it is not possible, please provide more information which JAXB/JAX-RS implementation you are using to be able to come up with some more concrete solution for your problem.
In general the idea is to control serialization process and decide how certain objects/fields are serialized and if those should be serialized at all. It can be achieved for example with following strategies:
Serialize class B not as a whole object, but rather just as a String representation of it, when class A is serialized. For example using #XmlAttribute #XmlIDREF.
Control serialization process by setting up, for example, some kind of Filter/Exclusion (depending on what does your JAX-RS implementation provide):
ExclusionStrategy() {
public boolean shouldSkipClass(Class<?> clazz) {
return (clazz == B.class);
}
/**
* Custom field exclusion goes here
*/
public boolean shouldSkipField(FieldAttributes f) {
return false;
}
}

Jackson - deserialization fails on circular dependencies

Ok, so I'm trying to test some stuffs with jackson json converter.
I'm trying to simulate a graph behaviour, so these are my POJO entities
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class ParentEntity implements java.io.Serializable
{
private String id;
private String description;
private ParentEntity parentEntity;
private List<ParentEntity> parentEntities = new ArrayList<ParentEntity>(0);
private List<ChildEntity> children = new ArrayList<ChildEntity>(0);
// ... getters and setters
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class ChildEntity implements java.io.Serializable
{
private String id;
private String description;
private ParentEntity parent;
// ... getters and setters
}
The tags are required in order to avoid exception on serialization.
When I try to serialize an object (both on a file or on a simple string) all works fine. However, when I try to deserialize the object, it throws an exception. This is the simple test method (try/catch omitted for simplicity)
{
// create some entities, assigning them some values
ParentEntity pe = new ParentEntity();
pe.setId("1");
pe.setDescription("first parent");
ChildEntity ce1 = new ChildEntity();
ce1.setId("1");
ce1.setDescription("first child");
ce1.setParent(pe);
ChildEntity ce2 = new ChildEntity();
ce2.setId("2");
ce2.setDescription("second child");
ce2.setParent(pe);
pe.getChildren().add(ce1);
pe.getChildren().add(ce2);
ParentEntity pe2 = new ParentEntity();
pe2.setId("2");
pe2.setDescription("second parent");
pe2.setParentEntity(pe);
pe.getParentEntities().add(pe2);
// serialization
ObjectMapper mapper = new ObjectMapper();
File f = new File("parent_entity.json");
// write to file
mapper.writeValue(f, pe);
// write to string
String s = mapper.writeValueAsString(pe);
// deserialization
// read from file
ParentEntity pe3 = mapper.readValue(f,ParentEntity.class);
// read from string
ParentEntity pe4 = mapper.readValue(s, ParentEntity.class);
}
and this is the exception thrown (of course, repeated twice)
com.fasterxml.jackson.databind.JsonMappingException: Already had POJO for id (java.lang.String) [com.fasterxml.jackson.annotation.ObjectIdGenerator$IdKey#3372bb3f] (through reference chain: ParentEntity["children"]->java.util.ArrayList[0]->ChildEntity["id"])
...stacktrace...
Caused by: java.lang.IllegalStateException: Already had POJO for id (java.lang.String) [com.fasterxml.jackson.annotation.ObjectIdGenerator$IdKey#3372bb3f]
...stacktrace...
So, what is the cause of the problem? How can I fix it? Do I need some other annotation?
Actually, it seems that the problem was with the "id" property. Because the name of the property is equal for the two different entities, there were some problems during deserialization. Don't know if it makes sense at all, but I solved the problem changing the JsonIdentityInfo tag of ParentEntity to
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = ParentEntity.class))
Of course, I also changed the scope of ChildEntity with scope=ChildEntity.class
as suggested here
I'm open to new answer and suggestions by the way.

Categories