Skip to content

fix-polymorphic-value-class-union-schema-name#479

Open
jantoebes wants to merge 5 commits intoavro-kotlin:mainfrom
jantoebes:fix-polymorphic-value-class-union-schema-name
Open

fix-polymorphic-value-class-union-schema-name#479
jantoebes wants to merge 5 commits intoavro-kotlin:mainfrom
jantoebes:fix-polymorphic-value-class-union-schema-name

Conversation

@jantoebes
Copy link
Copy Markdown

@jantoebes jantoebes commented Apr 30, 2026

  • Fix polymorphic resolver using the produced Avro schema name as union lookup key instead of the Kotlin serial name for value/inline classes
  • For a value class backed by a primitive (e.g. value class Label(val value: String)), the Avro union contains "string", not a named record. The resolver was building its map with the serial name as key, which never matched schema.fullName during decode, causing a SerializationException
  • The fix introduces avroUnionSchemaFullName() which unwraps inline classes recursively to resolve the correct Avro primitive type name (e.g. PrimitiveKind.STRING → "string"); named types (records, enums) are unchanged
  • Added a test covering a sealed hierarchy with a mix of a value class primitive, an enum, and a record subtype

Closes #480

Value-class wrappers generated inside a sealed interface for a complex
Avro union had no @AvroAlias pointing at the inner Avro full name, so
PolymorphicResolver could not map the incoming schema fullName
(e.g. "com.example.MyRecord", "int", "map") to the wrapper
SerialName ("ForMyRecord", "ForInt", "ForMap") at runtime.

This caused SerializationException: 'Serializer for subclass ... is not
found' on decode for every multi-type union.

Fix: emit @AvroAlias(subSchema.fullName) on each value-class wrapper so
AbstractPolymorphicDecoder can resolve the subtype via the existing
alias lookup in PolymorphicResolver.
…e for value class union members

For value/inline classes the Avro schema in a union is the unwrapped inner type (e.g. 'string' for a String-backed value class), not a named record. The resolver was building its lookup map with the value class serial name as key, which never matched the schema fullName during decode. Now the map key is derived from the produced Avro schema type, unwrapping inline classes recursively down to the primitive Avro type name.
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

.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

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[codegen] Missing @AvroAlias on generated union-branch wrapper classes causes SerializationException

2 participants