SMILEY4 / schema-kenerator

Analyze kotlin types, extract information and generate schemas
Apache License 2.0
8 stars 5 forks source link

Using Non-nullable and nullable type in the same class appends random numbers to the second type. #37

Closed K1rakishou closed 3 days ago

K1rakishou commented 6 days ago

Hi. After trying to update to the latest version, I have noticed this behavior.

Screenshot 2567-10-16 at 11 52 58

Screenshot 2567-10-16 at 11 53 12

They are both the same type, but one is nullable, the other one is non-nullable.

The problem seems to be in KotlinxSerializationTypeProcessingStep.kt

private fun getUniqueId(descriptor: SerialDescriptor, typeParameters: List<TypeId>, typeData: MutableList<BaseTypeData>): TypeId {
    if (knownNotParameterized.contains(descriptor.cleanSerialName())) {
        return TypeId.build(descriptor.cleanSerialName(), typeParameters)
    }
    if (typeData.find(descriptor, typeParameters) != null) {
        return TypeId.build(descriptor.cleanSerialName(), typeParameters, true)
    }
    return TypeId.build(descriptor.cleanSerialName(), typeParameters)
}

private fun Collection<BaseTypeData>.find(descriptor: SerialDescriptor, typeParameters: List<TypeId>): BaseTypeData? {
    val typeId = TypeId.build(descriptor.cleanSerialName(), typeParameters)
    return this.find { it.id.full() == typeId.full() }
}

When checking if the type is already defined

if (typeData.find(descriptor, typeParameters) != null) {

the nullability information is removed from full type name. Here we can see that the type is nullable: Screenshot 2567-10-16 at 13 17 54

But then when creating a type for searching descriptor.cleanSerialName() is used:

val typeId = TypeId.build(descriptor.cleanSerialName(), typeParameters)

where cleanSerialName() is removing the ?:

private fun SerialDescriptor.cleanSerialName() = this.serialName.replace("?", "")

Because of that typeData.find(descriptor, typeParameters) != null returns non-null value and getUniqueId returns TypeId with random value added to it (withAdditionalId is true).

If I remove nullability from second parameter then everything works as expected. Also if I reorder first and second fields in TestType then first field will have random numbers added to it instead of second which is nullable. So the problem is not in using nullability but in checking if a type was already defined to deduplicate them.

I have tried to use a typeRedirect from Something? -> Something but unfortunately it didn't help.

SMILEY4 commented 6 days ago

Hi, this comes from the limitation from kotlinx-serialization where it drops information about generic types, e.g.:

class TestType(
    val first: SomeType<String>,
    val second: SomeType<Int>,
)

I need to handle SomeType<String> and SomeType<Int> as two different types that generate different schemas, but kotlinx "only" keeps the information that it is SomeType. The workaround I decided on was to duplicate types that were already encountered, resulting in SomeType and SomeType#12345. Without that, information about the generic type could be mixed up and e.g. both SomeTypes would be incorrectly treated as SomeType<String> or SomeType<Int>. Unfortunatly, this behavior can also affect types/setups, where this would not be needed (like in your case).

I'm not sure if there is another reliable solution, approach or way to detect that two types have different/same/no type parameters :thinking:

A workaround to manually fix your problem would be to tell the processing step that the type does not have generic type parameters and that this behavior is not neccesary.

//...
.processKotlinxSerialization {
    markNotParameterized<Something>()
}
//...
K1rakishou commented 6 days ago

I see. Indeed, using markNotParameterized<Something>() solves the issue. Thank you!