spring-projects / spring-data-mongodb

Provides support to increase developer productivity in Java when using MongoDB. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-mongodb/
Apache License 2.0
1.59k stars 1.07k forks source link

Incorrect deserialization of a kotlin class when loading it from mongodb #4733

Closed gasabr closed 3 days ago

gasabr commented 1 week ago

I'm sorry if this is the wrong place to file an issue, I've not found the right one in 10 minutes of googling.

Environment

Here is the class I'm trying to read from the database

@Document(collection = "users")
internal data class User(
    @Id
    val userId: UserId,
    val telegramChatId: Long,
)

here is the repository

@Repository
internal interface UserRepository : CrudRepository<User, String> {
    fun findByTelegramChatId(telegramChatId: Long): User?
}

Saving works just fine, but when I'm trying to load it from the database, I'm getting a weird error Parameter org.springframework.data.mapping.Parameter@a972a9cc does not have a name, inside of the org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator#extractInvocationArguments. I've debugged the code and noticed that the last parameter in the extractInvocationArguments method is null and it identifies a default constructor. So, I added PersistenceCreator to my class like this

    companion object {
        @JvmStatic
        @PersistenceCreator
        fun create(
            userId: UserId,
            telegramChatId: Long,
        ) = User(userId, telegramChatId)
    }

and everything works correctly now. The root cause of the problem lies in spring data adding an argument to the default class constructor, PersistenceCreator fixes it, because it is being parsed by the spring correctly. If needed I can debug once again and point to the functions there I think resolution is incorrect. The UserId is a value class, which has dedicated converter, but it works as expected.

mp911de commented 1 week ago

Care to provide the UserId type? Ideally, you would provide a minimal reproducer as GitHub repo or zip file (attached to this issue) so that we have an exact reproducer.

One more thing: Looking at your dependency versions, it seems that you're using Spring Boot 3.0.4 that uses Spring Data Commons 3.0.4.

Kotlin value class support was introduced in a later version 3.2, as I'd anticipate inline/value class usage here.

gasabr commented 1 week ago

Providing minimal working example would take some time, because project is a bit messy rn, so here are the classes

internal class UserId(val id: String) {

    companion object {
        fun fromString(eventId: String?): UserId {
            if (eventId == null) {
                throw RuntimeException("UserId can not be null.")
            }
            return UserId(eventId)
        }

        fun createNew(): UserId = UserId("user-" + getRandomString(15))
    }
    // skipped toString, equals, hashCode, which I generated trying to find the root cause
}

Converters

@WritingConverter
internal class UserIdToStringConverter : Converter<UserId, String> {
    override fun convert(source: UserId): String {
        return source.id
    }
}

@ReadingConverter
internal class StringToUserIdConverter : Converter<String, UserId> {
    override fun convert(source: String): UserId {
        return UserId.fromString(source)
    }
}

Thanks for the ticket link, didn't know value classes were not supported yet, I'll pack the minimal reproducable example later today, but from the debugging I've done, it's not the "value" class, because UserId object is being restored correctly, the problem lies in parameters list having 3 values instead of 2, where first 2 are real parameters and the last one is null.

gasabr commented 3 days ago

i think i messed up my dependencies somewhere, because I can not create a minimal reproducible example with current versions of spring. Sorry, for bothering you, will reopen in case I make an repro:(