-
Notifications
You must be signed in to change notification settings - Fork 699
Description
I stumbled upon an issue that puzzled me for a bit, but I managed to reproduce the conditions under which it happens.
In #1947 support for Kotlin value classes was added. However, I strongly believe an assumption was made in the code, that doesn't hold true in all cases, leading to bug I encounter.
When a data class (or any class) defines a private primary constructor with parameters that do not have default values, the Kotlin compiler does not generate the synthetic constructor with the (int mask, DefaultConstructorMarker) signature. Instead, it only emits the (DefaultConstructorMarker) variant.
This leads to a runtime exception like the following:
org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate nl.edward.backend.domain.order.Order using constructor fun `<init>`(nl.edward.backend.domain.shared.Identifier<nl.edward.backend.domain.order.Order>, nl.edward.backend.domain.shared.Identifier<nl.edward.backend.domain.buyer.Buyer>, kotlin.collections.Set<nl.edward.backend.domain.order.Seller>, nl.edward.backend.domain.asset.Asset, java.math.BigDecimal, kotlin.String?, kotlin.String?, java.math.BigDecimal, kotlin.Boolean, java.time.LocalDate, kotlin.Boolean, kotlin.Long): nl.edward.backend.domain.order.Order with arguments 35a8e2d0-3013-4740-8a2a-3436afb267f2,dd4f5b94-d0e3-46ec-890a-94e5c0632306,[Seller(organizationId=59cd1422-74bc-48fb-a4ad-b85f2a37a5d4, quantity=50, unitPrice=10.00, purchaseInvoicePaidAt=null, purchaseInvoiceIsCreated=false, isDelivered=false)],HBE2025NL,15.00,null,TEST-REF-INTEGRATION,21.00,false,2025-10-27,false,1
at org.springframework.data.mapping.model.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:98) ~[spring-data-commons-3.5.4.jar:3.5.4]
at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:98) ~[spring-data-commons-3.5.4.jar:3.5.4]
at org.springframework.data.relational.core.conversion.MappingRelationalConverter.read(MappingRelationalConverter.java:471) ~[spring-data-relational-3.5.4.jar:3.5.4]
at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readAggregate(MappingRelationalConverter.java:366) ~[spring-data-relational-3.5.4.jar:3.5.4]
at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readAggregate(MappingRelationalConverter.java:329) ~[spring-data-relational-3.5.4.jar:3.5.4]
at org.springframework.data.jdbc.core.convert.MappingJdbcConverter.readAndResolve(MappingJdbcConverter.java:304) ~[spring-data-jdbc-3.5.4.jar:3.5.4]
at org.springframework.data.jdbc.core.convert.EntityRowMapper.mapRow(EntityRowMapper.java:65) ~[spring-data-jdbc-3.5.4.jar:3.5.4]
at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:94) ~[spring-jdbc-6.2.11.jar:6.2.11]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [nl.edward.backend.domain.order.Order]: Illegal arguments for constructor
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:220) ~[spring-beans-6.2.11.jar:6.2.11]
at org.springframework.data.mapping.model.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:96) ~[spring-data-commons-3.5.4.jar:3.5.4]
... 302 common frames omitted
Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.checkReceiver(DirectMethodHandleAccessor.java:197) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:99) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at kotlin.reflect.jvm.internal.calls.ValueClassAwareCaller.call(ValueClassAwareCaller.kt:200) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.KCallableImpl.callDefaultMethod$kotlin_reflection(KCallableImpl.kt:250) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at kotlin.reflect.jvm.internal.KCallableImpl.callBy(KCallableImpl.kt:155) ~[kotlin-reflect-2.2.20.jar:2.2.20-release-333]
at org.springframework.beans.BeanUtils$KotlinDelegate.instantiateClass(BeanUtils.java:943) ~[spring-beans-6.2.11.jar:6.2.11]
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:191) ~[spring-beans-6.2.11.jar:6.2.11]
... 303 common frames omitted
I'm not enough of a Kotlin/reflection expert to pin-point the fix, however I believe it has something to do with this piece of code found in KotlinInstantiationDelegate
if (!KotlinInstantiationDelegate.hasDefaultConstructorMarker(detectedConstructorParameters)) {
// candidates must contain at least two additional parameters (int, DefaultConstructorMarker).
// Number of defaulting masks derives from the original constructor arg count
int syntheticParameters = KotlinDefaultMask.getMaskCount(detectedConstructor.getParameterCount())
+ /* DefaultConstructorMarker */ 1;
if ((detectedConstructor.getParameterCount() + syntheticParameters) != candidate.getParameterCount()) {
continue;
}
}Notably, the code assumes there are always at least two synthetic parameters in this case, one for the default constructor marker (which is correct), and one for the mask (KotlinDefaultMask.getMaskCount always returns at least 1 which is apparently incorrect in the case of a private default constructor with no default params).
I guess one way or another we might need to detect the latter cases and prevent assuming an extra synthetic parameter that does not exist in reality.
For what it's worth, modifying the value of syntheticParameters at runtime with the debugger to be one less, solves the issue.
Shortly, I will link a pull request (#3390) showcasing two additional test cases that show the issue. One of the test cases displays the issue, the other shows that the issue does not happen under the exact same circumstances other than a default constructor param value being added.