I am on Spring Boot 2.0.6, where an entity pet
do have a Lazy many-to-one relationship to another entity owner
Pet entity
@Entity
@Table(name = "pets")
public class Pet extends AbstractPersistable<Long> {
@NonNull
private String name;
private String birthday;
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
@JsonIdentityReference(alwaysAsId=true)
@JsonProperty("ownerId")
@ManyToOne(fetch=FetchType.LAZY)
private Owner owner;
But while submitting a request like /pets
through a client(eg: PostMan), the controller.get() method run into an exception as is given below:-
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.lang.Long and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0]->com.petowner.entity.Pet["ownerId"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.9.7.jar:2.9.7]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191) ~[jackson-databind-2.9.7.jar:2.9.7]
Controller.get implementation
@GetMapping("/pets")
public @ResponseBody List<Pet> get() {
List<Pet> pets = petRepository.findAll();
return pets;
}
My observations
Tried to invoke explicitly the getters within
owner
throughpet
to force the lazy-loading from the javaassist proxy object ofowner
within thepet
. But did not work.@GetMapping("/pets") public @ResponseBody List<Pet> get() { List<Pet> pets = petRepository.findAll(); pets.forEach( pet -> pet.getOwner().getId()); return pets; }
Tried as suggested by this stackoverflow answer at https://stackoverflow.com/a/51129212/5107365 to have controller call to delegate to a service bean within the transaction scope to force lazy-loading. But that did not work too.
@Service @Transactional(readOnly = true) public class PetServiceImpl implements PetService { @Autowired private PetRepository petRepository; @Override public List<Pet> loadPets() { List<Pet> pets = petRepository.findAll(); pets.forEach(pet -> pet.getOwner().getId()); return pets; }
}
It works when Service/Controller returning a DTO created out from the entity. Obviously, the reason is JSON serializer get to work with a POJO instead of an ORM entity without any mock objects in it.
Changing the entity fetch mode to FetchType.EAGER would solve the problem, but I did not want to change it.
I am curious to know why it is thrown the exception in case of (1) and (2). Those should have forced the explicit loading of lazy objects.
Probably the answer might be connected to the life and scope of that javassist objects got created to maintain the lazy objects. Yet, wondering how would Jackson serializer not find a serializer for a java wrapper type like java.lang.Long
. Please do rememeber here that the exception thrown did indicate that Jackson serializer got access to owner.getId
as it recognised the type of the property ownerId
as java.lang.Long
.
Any clues would be highly appreciated.
Edit
The edited part from the accepted answer explains the causes. Suggestion to use a custom serializer is very useful one in case if I don't need to go in DTO's path.
I did a bit of scanning through the Jackson sources to dig down to the root causes. Thought to share that too.
Jackson caches most of the serialization metadata on first use. Logic related to the use case in discussion starts at this method com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(Collection<?> value, JsonGenerator g, SerializerProvider provider)
. And, the respective code snippet is:-
The statement serializer = _findAndAddDynamic(serializers, cc, provider)
at Line #140 trigger the flow to assign serializers for pet
-level properties while skipping ownerId
to be later processed through serializer.serializeWithType
at line #147.
Assigning of serializers is done at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.resolve(SerializerProvider provider)
method. The respective snippet is shown below:-
Serializers are assigned at line #340 only for those properties which are confirmed as final
through the check at line #333.
When owner
comes here, its proxied properties are found to be of type com.fasterxml.jackson.databind.type.SimpleType
. Had this associated entity been loaded eagerly
, the proxied properties obviously won't be there. Instead, original properties would be found with the values that are typed with final classes like Long, String, etc. (just like the pet
properties).
Wondering why can't Jackson address this from their end by using the getter's type instead of using that of the proxied property. Anyway, that could be a different topic to discuss :-)