diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java
index c73bb8bb..811e6cda 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java
@@ -1,7 +1,9 @@
package tech.ydb.yoj.databind.schema.reflect;
+import com.google.common.reflect.TypeToken;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KCallable;
+import kotlin.reflect.KClass;
import kotlin.reflect.jvm.KCallablesJvm;
import kotlin.reflect.jvm.ReflectJvmMapping;
import tech.ydb.yoj.databind.FieldValueType;
@@ -22,7 +24,7 @@ public final class KotlinDataClassComponent implements ReflectField {
private final Class> type;
private final FieldValueType valueType;
private final Column column;
-
+
private final ReflectType> reflectType;
public KotlinDataClassComponent(Reflector reflector, String name, KCallable> callable) {
@@ -33,7 +35,15 @@ public KotlinDataClassComponent(Reflector reflector, String name, KCallable> c
var kReturnType = callable.getReturnType();
this.genericType = ReflectJvmMapping.getJavaType(kReturnType);
- this.type = JvmClassMappingKt.getJavaClass(kReturnType);
+
+ var kClassifier = kReturnType.getClassifier();
+ if (kClassifier instanceof KClass> kClass) {
+ this.type = JvmClassMappingKt.getJavaClass(kClass);
+ } else {
+ // fallback to Guava's TypeToken if kotlin-reflect returns unpredictable results ;-)
+ this.type = TypeToken.of(genericType).getRawType();
+ }
+
this.column = type.getAnnotation(Column.class);
this.valueType = FieldValueType.forJavaType(genericType, column);
this.reflectType = reflector.reflectFieldType(genericType, valueType);
diff --git a/repository/pom.xml b/repository/pom.xml
index ba4117f8..0af379a0 100644
--- a/repository/pom.xml
+++ b/repository/pom.xml
@@ -39,6 +39,18 @@
simpleclient
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ true
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ true
+
+
org.eclipse.collections
eclipse-collections
@@ -70,4 +82,31 @@
test
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+ compileTests
+ process-test-sources
+
+ test-compile
+
+
+
+
+
+
+
+
+ 17
+
+
+
+
+
+
diff --git a/repository/src/test/java/tech/ydb/yoj/repository/hybrid/ChangeMetadata.java b/repository/src/test/java/tech/ydb/yoj/repository/hybrid/ChangeMetadata.java
new file mode 100644
index 00000000..938061a1
--- /dev/null
+++ b/repository/src/test/java/tech/ydb/yoj/repository/hybrid/ChangeMetadata.java
@@ -0,0 +1,15 @@
+package tech.ydb.yoj.repository.hybrid;
+
+import java.time.Instant;
+
+public record ChangeMetadata(
+ ChangeKind kind,
+ Instant time,
+ long version
+) {
+ enum ChangeKind {
+ CREATE,
+ UPDATE,
+ DELETE,
+ }
+}
diff --git a/repository/src/test/java/tech/ydb/yoj/repository/hybrid/Chronology.java b/repository/src/test/java/tech/ydb/yoj/repository/hybrid/Chronology.java
new file mode 100644
index 00000000..35bdde0e
--- /dev/null
+++ b/repository/src/test/java/tech/ydb/yoj/repository/hybrid/Chronology.java
@@ -0,0 +1,9 @@
+package tech.ydb.yoj.repository.hybrid;
+
+import java.time.Instant;
+
+public record Chronology(
+ Instant createdAt,
+ Instant updatedAt
+) {
+}
diff --git a/repository/src/test/kotlin/tech/ydb/yoj/repository/hybrid/Account.kt b/repository/src/test/kotlin/tech/ydb/yoj/repository/hybrid/Account.kt
new file mode 100644
index 00000000..253d6ecd
--- /dev/null
+++ b/repository/src/test/kotlin/tech/ydb/yoj/repository/hybrid/Account.kt
@@ -0,0 +1,28 @@
+package tech.ydb.yoj.repository.hybrid
+
+import tech.ydb.yoj.repository.db.Entity
+
+data class Account(
+ private val id: Id,
+ private val version: Long,
+ private val chronology: Chronology,
+ val login: String,
+ val description: String? = null,
+) : Entity {
+ override fun getId() = id
+
+ data class Id(val value: String) : Entity.Id
+
+ data class Snapshot(
+ private val id: Id,
+ private val entity: Account?,
+ val meta: ChangeMetadata,
+ ) : Entity {
+ override fun getId() = id
+
+ data class Id(
+ private val id: Account.Id,
+ private val version: Long
+ ) : Entity.Id
+ }
+}
diff --git a/repository/src/test/kotlin/tech/ydb/yoj/repository/hybrid/HybridSchemaTest.kt b/repository/src/test/kotlin/tech/ydb/yoj/repository/hybrid/HybridSchemaTest.kt
new file mode 100644
index 00000000..a24bbd09
--- /dev/null
+++ b/repository/src/test/kotlin/tech/ydb/yoj/repository/hybrid/HybridSchemaTest.kt
@@ -0,0 +1,45 @@
+package tech.ydb.yoj.repository.hybrid
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import tech.ydb.yoj.repository.db.EntitySchema
+import java.time.Instant
+
+/**
+ * Test for Schema using both Kotlin data classes and JVM records.
+ */
+class HybridSchemaTest {
+ @Test
+ fun testIdFlattening() {
+ val now = Instant.now()
+ val account = Account(
+ id = Account.Id("hahaha"),
+ version = 1L,
+ chronology = Chronology(now, now),
+ login = "logIn",
+ description = null
+ )
+ val accountSnapshot = Account.Snapshot(
+ id = Account.Snapshot.Id(account.id, 1L),
+ entity = account,
+ meta = ChangeMetadata(ChangeMetadata.ChangeKind.CREATE, now, 1L)
+ )
+
+ val schema = EntitySchema.of(Account.Snapshot::class.java)
+
+ val flattened = schema.flatten(accountSnapshot)
+ assertThat(flattened).isEqualTo(mapOf(
+ "id_id" to "hahaha",
+ "id_version" to 1L,
+ "entity_id" to "hahaha",
+ "entity_version" to 1L,
+ "entity_chronology_createdAt" to now,
+ "entity_chronology_updatedAt" to now,
+ "entity_login" to "logIn",
+ "meta_kind" to ChangeMetadata.ChangeKind.CREATE,
+ "meta_time" to now,
+ "meta_version" to 1L
+ ))
+ assertThat(schema.newInstance(flattened)).isEqualTo(accountSnapshot)
+ }
+}