Skip to content

Failed to instantiate bug when a class with private constructor has value class constructor param #3389

@edwardmp

Description

@edwardmp

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions