[JAVA-5128] Add annotations to support default values for primitive types Created: 31/Aug/23  Updated: 05/Sep/23

Status: Backlog
Project: Java Driver
Component/s: POJO
Affects Version/s: 4.10.0
Fix Version/s: None

Type: New Feature Priority: Major - P3
Reporter: Ben Weidig Assignee: Jeffrey Yemin
Resolution: Unresolved Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified


 Description   

Summary

If a Record evolves by gaining a new component, the RecordCodec will trigger an IllegalArgumentException in case of a new primitive component, as the missing field is translated into null.

Please provide the version of the driver. If applicable, please provide the MongoDB server version and topology (standalone, replica set, or sharded cluster).

4.10.0, standalone, running in Docker

How to Reproduce

// INITIAL/STORED RECORD
public record Aspect(@BsonId @BsonRepresentation(BsonType.OBJECT_ID)
                     String id,
                     Long legacyId,
                     int order) implements Serializable { }
 
 
// RECORD WITH ADDITIONAL PRIMITIVE FIELD
public record Aspect(@BsonId @BsonRepresentation(BsonType.OBJECT_ID)
                     String id,
                     Long legacyId,
                     int order,
                     boolean notFitlerable) implements Serializable { }

Any retrieval from a MongoCollection<Aspect> triggers the exception.

Additional Background

The newInstance call in https://github.com/mongodb/mongo-java-driver/blob/master/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java#L285 will result in an IllegalArgumentException, as the constructorArguments contain null for the new component.

Caused by: java.lang.IllegalArgumentException
	at jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[?:?]
	at jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[?:?]
	at java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[?:?]
	at java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[?:?]
	at org.bson.codecs.record.RecordCodec.decode(RecordCodec.java:285) ~[bson-record-codec-4.10.0.jar:?]

The problem, in my opinion, is that the componentModel uses wrapper types, but the constructor is detected with the original/non-wrapper types, so adding another constructor that uses wrapper types isn't a feasible workaround.



 Comments   
Comment by Ben Weidig [ 02/Sep/23 ]

Hi jeff.yemin@mongodb.com,

Thank you for the explanation of the current behavior.

My reasoning was how evolving Records behave during regular serialization.
However, after your comment and thinking a bit more about it, I concur that a more deterministic and explicit behavior is required in a database context.

Looking at other serialization libraries, annotations seem to be the way to go for default values.
Still, adding an annotation per primitive type might be overkill and not as discoverable/straightforward as a single one.

I haven't dug too deep into the code, so I'm not even sure my following (rough) ideas are feasible or makes sense in the general context...

Thinking about possible use cases, I come up with the same you mentioned:

  • Use primitive default
  • Use custom primitive value

If only the first one is required, I think a single annotation like @BsonDefaultValue will suffice, as the user must not provide an actual default value.

In the case of needing a custom value, having multiple annotations for each type would be a necessity, as annotations don't allow Object as a value.

But thinking further than just primitives, how about supplying a default value for a nested Record or Object-based component?
Even though null works here, it might not be what a user wants/needs.

The @BsonDefaultValue could accept a org.bson.codecs.Decoder<T> class that creates the value if the document is missing the field:

public @interface BsonDefaultValue {
    Class<?> decoder() default void.class;
}

This way, not a full-blown Codec is required from the user, but they can build a default value provider as complex as they need.
The downside, though, is that the Decoders must be stored somewhere, similar to a CodecRegistry.
And even though it's more flexible for complex cases, it's more work for constant values.

Long story short, any kind of default value support is a welcomed addition to the driver.
Workarounds are still possible because I can either update the affected documents before releasing the new evolved Record or provide my own Codec for a type if absolutely necessary.

Having a simple annotation-based solution, a single one or one per primitive type, would be the easiest approach from a user perspective IMO.
Creating a new infrastructure for default value Decoders might be too high-impact and create too much technical debt in the long run.

Comment by Jeffrey Yemin [ 01/Sep/23 ]

Hi ben@netzgut.net, this behavior is intentional and consistent with the PojoCodec (for which we have a test that asserts the behavor). The reason it works this way is because otherwise there would be no way to distinguish between an actual value decoded from the database and the default value for the primitive type. Moreover, you might want the default value for a missing field to be something other than the default value for primitives in Java, e.g. in the case above, you might want the component name to be filterable and default to true.

But your use case is certainly valid, so the driver should provide some solution. One idea is to add a new annotation that allows the application to tell the decoder that it's allowed to be missing, and to supply a default value as well. Something like:

public record Aspect(@BsonId @BsonRepresentation(BsonType.OBJECT_ID)
                     // ...
                     @BsonDefaultBooleanValue(value = true)
                     boolean filterable) { }

And something similar for the other primitive types.

Let us know what you think about this idea.

Comment by PM Bot [ 31/Aug/23 ]

Hi ben@netzgut.net, thank you for reporting this issue! The team will look into it and get back to you soon.

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