Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.github.avrokotlin.avro4k.internal

import com.github.avrokotlin.avro4k.AvroAlias
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.modules.SerializersModule

Expand All @@ -12,12 +14,46 @@ internal class PolymorphicResolver(private val serializersModule: SerializersMod
descriptor.possibleSerializationSubclasses(serializersModule)
.flatMap {
sequence {
yield(it.nonNullSerialName to it.nonNullSerialName)
yield(it.avroUnionSchemaFullName() to it.nonNullSerialName)
Copy link
Copy Markdown
Member

@Chuckame Chuckame Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you are trying to resolve a descriptor to its schema name. The best is to pass a schemaNameResolver: (SerialDescriptor) -> String, and in the Avro class, pass { schema(it).name } as there are many things to do to resolve a name. Example: for Fixed type, it's a ByteArray with the @AvroFixed annotation. any type with @AvroString is finally a string. etc

it.findAnnotation<AvroAlias>()?.value?.forEach { alias ->
yield(alias to it.nonNullSerialName)
}
}
}.toMap()
}
}
}
}

/**
* Returns the Avro schema full name that this descriptor produces when used inside a union.
*
* For value/inline classes, the Avro schema is the unwrapped inner type's schema, so the union
* lookup key must match that inner schema's full name (e.g. "string" for a String-backed value class)
* rather than the value class's own serial name.
*
* For all other types the serial name is used directly, matching how [ClassVisitor] and [ValueVisitor]
* produce named Avro schemas.
*/
@OptIn(ExperimentalSerializationApi::class)
private fun SerialDescriptor.avroUnionSchemaFullName(): String {
if (!isInline) return nonNullSerialName
val innerDescriptor = getElementDescriptor(0)
return when (val kind = innerDescriptor.kind) {
is PrimitiveKind -> kind.avroTypeName()
else -> innerDescriptor.avroUnionSchemaFullName()
}
}

private fun PrimitiveKind.avroTypeName(): String =
when (this) {
PrimitiveKind.BOOLEAN -> "boolean"
PrimitiveKind.BYTE,
PrimitiveKind.SHORT,
PrimitiveKind.INT,
PrimitiveKind.CHAR,
-> "int"
PrimitiveKind.LONG -> "long"
PrimitiveKind.FLOAT -> "float"
PrimitiveKind.DOUBLE -> "double"
PrimitiveKind.STRING -> "string"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.github.avrokotlin.avro4k.recordWithSchema
import com.github.avrokotlin.avro4k.schema
import io.kotest.core.spec.style.StringSpec
import kotlinx.serialization.Serializable
import org.apache.avro.generic.GenericData

internal class SealedClassEncodingTest : StringSpec({
"encode/decode sealed classes" {
Expand All @@ -15,6 +16,13 @@ internal class SealedClassEncodingTest : StringSpec({
AvroAssertions.assertThat<Operation>(Operation.Binary.Add(1, 2))
.isEncodedAs(recordWithSchema(Avro.schema<Operation.Binary.Add>(), 1, 2))
}
"encode/decode sealed class union with value class primitive, enum and record subtypes" {
val shapeSchema = Avro.schema<Shape>()
val colorSchema = shapeSchema.types.first { it.name == "Color" }
AvroAssertions.assertThat<Shape>(Shape.Label("hello")).isEncodedAs("hello")
AvroAssertions.assertThat<Shape>(Shape.Color.RED).isEncodedAs(GenericData.get().createEnum("RED", colorSchema))
AvroAssertions.assertThat<Shape>(Shape.Circle(5)).isEncodedAs(recordWithSchema(Avro.schema<Shape.Circle>(), 5))
}
"encode/decode nullable sealed classes" {
AvroAssertions.assertThat(ReferencingNullableSealedClass(Operation.Binary.Add(1, 2)))
.isEncodedAs(record(recordWithSchema(Avro.schema<Operation.Binary.Add>(), 1, 2)))
Expand All @@ -27,6 +35,19 @@ internal class SealedClassEncodingTest : StringSpec({
.isEncodedAs(null)
}
}) {
@Serializable
private sealed interface Shape {
@JvmInline
@Serializable
value class Label(val value: String) : Shape

@Serializable
enum class Color : Shape { RED, BLUE }

@Serializable
data class Circle(val radius: Int) : Shape
}

@Serializable
private data class ReferencingSealedClass(
val notNullable: Operation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ public class KotlinGenerator(
* ```kotlin
* @Serializable
* sealed interface <potentialAnonymousBaseName>Union {
* @AvroAlias("<Type full name>")
* @JvmInline
* @Serializable
* value class For<Type name>(val value: <Type full name>) : <potentialAnonymousBaseName>Union
Expand All @@ -356,6 +357,7 @@ public class KotlinGenerator(
TypeSpec.classBuilder(unionSubTypeNameFormatter(if (hasSimilarNames) subSchema.fullName else subSchema.simpleName.toPascalCase()))
.addSuperinterface(ClassName.fromFullName(className))
.addModifiers(KModifier.VALUE)
.addAnnotationIfNotNull(buildAvroAliasAnnotation(listOf(subSchema.fullName)))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avro aliases are avro aliases, not for workaround. What are you trying to solve with this alias?

Because the polymorphic resolver should actually resolve this workaround

.addAnnotation(JvmInline::class)
.addAnnotation(Serializable::class)
.addPrimaryProperty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.AvroDefault
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
Expand All @@ -25,18 +26,21 @@ public data class ComplexUnionInRecord(
@Serializable
@AvroGenerated("""["null",{"type":"record","name":"NestedRecord","fields":[{"name":"id","type":"string"},{"name":"value","type":"int"}]},{"type":"enum","name":"Status","symbols":["ACTIVE","INACTIVE","PENDING"]},{"type":"array","items":"string"}]""")
public sealed interface TheFieldUnion {
@AvroAlias("NestedRecord")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to re-run actionsBeforeCommit

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok done! thx for the review :-D

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why the avro alias annotation is still present?

@JvmInline
@Serializable
public value class ForNestedRecord(
public val `value`: NestedRecord,
) : TheFieldUnion

@AvroAlias("Status")
@JvmInline
@Serializable
public value class ForStatus(
public val `value`: Status,
) : TheFieldUnion

@AvroAlias("array")
@JvmInline
@Serializable
public value class ForArray(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ public data class CamelCaseRecord(
@Serializable
@AvroGenerated("""["string",{"type":"enum","name":"StatusFlag","namespace":"ns","symbols":["ACTIVE","INACTIVE"]},"null"]""")
public sealed interface AccountStatusUnion {
@AvroAlias("string")
@JvmInline
@Serializable
public value class ForString(
public val `value`: String,
) : AccountStatusUnion

@AvroAlias("ns.StatusFlag")
@JvmInline
@Serializable
public value class ForStatusFlag(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.AvroFixed
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
Expand All @@ -20,43 +21,50 @@ import kotlinx.serialization.Serializable
@Serializable
@AvroGenerated("""["null","string","int",{"type":"record","name":"NestedRecord","fields":[{"name":"field","type":"string","doc":"field doc"}]},{"type":"enum","name":"AnEnum","symbols":["A","B","C"]},{"type":"fixed","name":"AFixed","size":5},{"type":"array","items":"int"},{"type":"map","values":["null","double"]}]""")
public sealed interface TestSchema {
@AvroAlias("string")
@JvmInline
@Serializable
public value class ForString(
public val `value`: String,
) : TestSchema

@AvroAlias("int")
@JvmInline
@Serializable
public value class ForInt(
public val `value`: Int,
) : TestSchema

@AvroAlias("NestedRecord")
@JvmInline
@Serializable
public value class ForNestedRecord(
public val `value`: NestedRecord,
) : TestSchema

@AvroAlias("AnEnum")
@JvmInline
@Serializable
public value class ForAnEnum(
public val `value`: AnEnum,
) : TestSchema

@AvroAlias("AFixed")
@JvmInline
@Serializable
public value class ForAFixed(
@AvroFixed(size = 5)
public val `value`: ByteArray,
) : TestSchema

@AvroAlias("array")
@JvmInline
@Serializable
public value class ForArray(
public val `value`: List<Int>,
) : TestSchema

@AvroAlias("map")
@JvmInline
@Serializable
public value class ForMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
import com.github.avrokotlin.avro4k.`internal`.AvroGenerated
Expand All @@ -15,12 +16,14 @@ import kotlinx.serialization.Serializable
@Serializable
@AvroGenerated("""["double","null","int"]""")
public sealed interface TestSchemaMapUnion {
@AvroAlias("double")
@JvmInline
@Serializable
public value class ForDouble(
public val `value`: Double,
) : TestSchemaMapUnion

@AvroAlias("int")
@JvmInline
@Serializable
public value class ForInt(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
import com.github.avrokotlin.avro4k.`internal`.AvroGenerated
Expand All @@ -17,18 +18,21 @@ import kotlinx.serialization.Serializable
@Serializable
@AvroGenerated("""["int","null",{"type":"map","values":["string","null",{"type":"array","items":["long","null",{"type":"map","values":["boolean","null"]}]}]},{"type":"array","items":["long","null","double"]}]""")
public sealed interface TestSchema {
@AvroAlias("int")
@JvmInline
@Serializable
public value class ForInt(
public val `value`: Int,
) : TestSchema

@AvroAlias("map")
@JvmInline
@Serializable
public value class ForMap(
public val `value`: Map<String, TestSchemaMapUnion?>,
) : TestSchema

@AvroAlias("array")
@JvmInline
@Serializable
public value class ForArray(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
import com.github.avrokotlin.avro4k.`internal`.AvroGenerated
Expand All @@ -15,12 +16,14 @@ import kotlinx.serialization.Serializable
@Serializable
@AvroGenerated("""["long","null","double"]""")
public sealed interface TestSchemaArrayUnion {
@AvroAlias("long")
@JvmInline
@Serializable
public value class ForLong(
public val `value`: Long,
) : TestSchemaArrayUnion

@AvroAlias("double")
@JvmInline
@Serializable
public value class ForDouble(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
import com.github.avrokotlin.avro4k.`internal`.AvroGenerated
Expand All @@ -17,12 +18,14 @@ import kotlinx.serialization.Serializable
@Serializable
@AvroGenerated("""["long","null",{"type":"map","values":["boolean","null"]}]""")
public sealed interface TestSchemaMapArrayUnion {
@AvroAlias("long")
@JvmInline
@Serializable
public value class ForLong(
public val `value`: Long,
) : TestSchemaMapArrayUnion

@AvroAlias("map")
@JvmInline
@Serializable
public value class ForMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
import com.github.avrokotlin.avro4k.`internal`.AvroGenerated
Expand All @@ -15,12 +16,14 @@ import kotlinx.serialization.Serializable
@Serializable
@AvroGenerated("""["string","null",{"type":"array","items":["long","null",{"type":"map","values":["boolean","null"]}]}]""")
public sealed interface TestSchemaMapUnion {
@AvroAlias("string")
@JvmInline
@Serializable
public value class ForString(
public val `value`: String,
) : TestSchemaMapUnion

@AvroAlias("array")
@JvmInline
@Serializable
public value class ForArray(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ExperimentalAvro4kApi::class,
)

import com.github.avrokotlin.avro4k.AvroAlias
import com.github.avrokotlin.avro4k.AvroFixed
import com.github.avrokotlin.avro4k.ExperimentalAvro4kApi
import com.github.avrokotlin.avro4k.InternalAvro4kApi
Expand All @@ -18,37 +19,43 @@ import ns2.Enum
@Serializable
@AvroGenerated("""[{"type":"record","name":"Record","namespace":"ns","fields":[{"name":"field","type":"int"}]},{"type":"record","name":"RecordWithoutNamespace","fields":[{"name":"field","type":"int"}]},{"type":"enum","name":"Enum","namespace":"ns2","symbols":["FIRST","SECOND"]},{"type":"fixed","name":"FixedType","size":12},"string","bytes"]""")
public sealed interface Union {
@AvroAlias("ns.Record")
@JvmInline
@Serializable
public value class ForRecord(
public val `value`: Record,
) : Union

@AvroAlias("RecordWithoutNamespace")
@JvmInline
@Serializable
public value class ForRecordWithoutNamespace(
public val `value`: RecordWithoutNamespace,
) : Union

@AvroAlias("ns2.Enum")
@JvmInline
@Serializable
public value class ForEnum(
public val `value`: Enum,
) : Union

@AvroAlias("FixedType")
@JvmInline
@Serializable
public value class ForFixedType(
@AvroFixed(size = 12)
public val `value`: ByteArray,
) : Union

@AvroAlias("string")
@JvmInline
@Serializable
public value class ForString(
public val `value`: String,
) : Union

@AvroAlias("bytes")
@JvmInline
@Serializable
public value class ForBytes(
Expand Down