[JAVA-4578] Java driver cannot handle Deeper Polymorphic trees Created: 18/Apr/22  Updated: 21/Aug/23

Status: Backlog
Project: Java Driver
Component/s: BSON, Codecs, POJO
Affects Version/s: 4.3.4
Fix Version/s: None

Type: Improvement Priority: Major - P3
Reporter: Greg Stewart Assignee: Ross Lawley
Resolution: Unresolved Votes: 1
Labels: external-user
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Depends
depends on JAVA-3853 Refactor Pojo Codec Backlog
Related
related to JAVA-5110 Double inheritance with generic types... Backlog
Epic Link: Investigate our POJO implementation
Quarter: FY24Q2
Documentation Changes Summary:

1. What would you like to communicate to the user about this feature?
2. Would you like the user to see examples of the syntax and/or executable code and its output?
3. Which versions of the driver/connector does this apply to?


 Description   

The Pojo codec is limited in handling multiple level generic types that are passed through the class heirarchy.

Take the following classes:

abstract class GenericBaseModel<A, B, C> {
 
    private A alpha;
    private B bravo;
    private C charlie;
   
   // ... getters & setters
}
 
public abstract class GenericPassThroughModel<A, B, C> extends GenericBaseModel<C, B, A> {
 
}
 
public final class GenericPassThroughModelImpl extends GenericPassThroughModel<List<String>, Integer, String> {
 
}

The concrete types of GenericPassThroughModelImpl are lost due to the PojoCodecs type detection.


See first comment for the previous description.



 Comments   
Comment by Greg Stewart [ 07/Oct/22 ]

Hey! Hope things are going well! Was wondering about the status of this, as I end up hitting this issue fairly regularly.

I did find somewhat of a workaround though; depending on semantics, you can make the methods of a superclass abstract, then actually add the field of the implementing class and override the required methods there. This enforces the same behavior as before, and lets Mongo make sense of the concrete class, instead of losing that field's type information. Not optimal due to needing to implement more things, but still achieve some of the benefits of inheritance.

Comment by Greg Stewart [ 28/Apr/22 ]

Hey Ross,

Thanks for taking a look at it, glad to hear it was reproducible!

Comment by Ross Lawley [ 28/Apr/22 ]

Hi contact@gjstewart.net,

Thank you for the ticket and the code showing the issue. I've simplified the example and updated the description.

The improvements required for the PojoCodec to handle this will need to be scheduled and planned as its would require some heavy rewriting of the ClassModel building logic.

Ross

Comment by Ross Lawley [ 28/Apr/22 ]

*Description was: *

Apologies if this isn't quite the avenue for this; I have gone through more 'normal' channels and haven't gotten a response:

Summary

It seems that the current Java driver (versioned to what is used by current versions of Quarkus) cannot process polymorphic objects deeper than one level:

           InventoryItem (abstract)
                  /                               \
      Tracked Item            AmountItem (Abstract)
                                                     /             \
                        SimpleAmountItem       ListAmountItem

This is realized as such: (Full code on Github)

@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "storedType"
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = SimpleAmountItem.class, name = "AMOUNT_SIMPLE"),
    @JsonSubTypes.Type(value = ListAmountItem.class, name = "AMOUNT_LIST"),
    @JsonSubTypes.Type(value = TrackedItem.class, name = "TRACKED")
})
@BsonDiscriminator
public abstract class InventoryItem<T> extends ImagedMainObject {
    @NonNull
    @NotNull
    private Map<@NonNull ObjectId, @NonNull T> storageMap = new LinkedHashMap<>();
    private final StoredType storedType;
    //...
}
 
@EqualsAndHashCode(callSuper = true)
@Data
public class TrackedItem extends InventoryItem<Map<@NotBlank String, @NotNull TrackedStored>> {
    //...
}
 
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ValidHeldStoredUnits
public abstract class AmountItem<T> extends InventoryItem<T> {
    //...
}
 
@EqualsAndHashCode(callSuper = true)
@Data
public class SimpleAmountItem extends AmountItem<AmountStored> {
    //...
}
 
@EqualsAndHashCode(callSuper = true)
@Data
public class ListAmountItem extends AmountItem<List<@NotNull AmountStored>> {
    //...
}

TrackedItem is able to be CRUD'ed just fine, but both SimpleAmountItem and ListAmountItem produce the same following error when trying to create:

Encoding a ListAmountItem: 'ListAmountItem()' failed with the following exception:
 
Failed to encode 'ListAmountItem'. Encoding 'storageMap' errored with: Can't find a codec for class java.lang.Object.
 
A custom Codec or PojoCodec may need to be explicitly configured and registered to handle this type.
    at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:105)
    at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:359)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:218)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:519)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161)
    at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
    at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247)
    at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:73)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:151)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler$1.run(VertxRequestHandler.java:91)
    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$13.runWith(VertxCoreRecorder.java:543)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: org.bson.codecs.configuration.CodecConfigurationException: An exception occurred when encoding using the AutomaticPojoCodec.
Encoding a ListAmountItem: 'ListAmountItem()' failed with the following exception:
 
Failed to encode 'ListAmountItem'. Encoding 'storageMap' errored with: Can't find a codec for class java.lang.Object.

It appears that Mongo can reconcile direct/first descendants of a superclass, but not if the inheritance tree gets any deeper than that. Is this as designed, a bug, or something I can tweak to get around?

It appears to me that Mongo gets stuck on trying to reconcile the Simple/ListAmountItems as a plain AmountItem, which makes sense as why it's failing, but not terribly clear as to how to fix it. The @BsonDiscriminator seems rather simplistic, esp. compared to Jackson.

Looks like there might be some support for specifying known types, but I don't see an analogous java annotation: https://mongodb.github.io/mongo-csharp-driver/2.6/reference/bson/mapping/polymorphism/

Please provide the version of the driver. If applicable, please provide the MongoDB

org.mongodb:bson:4.3.2, locked to what is used by Quarkus `2.8.0.Final`

How to Reproduce

Have a deeper polymorphic tree, attempt to perform a CREATE on the deeper objects.

Failing tests on Github Actions:

https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/runs/6064459996#r1s0

My source is on Github:

You can run ./gradlew test to run this (in the base station Quarkus server), and see the errors.

Note you will need to run ./gradlew publishMavenLocal on the lib project first: https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/tree/main/software/libs/open-qm-core

Generated at Thu Feb 08 09:02:27 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.