micronaut-projects / micronaut-serialization

Build Time Serialization APIs for Micronaut
Apache License 2.0
26 stars 18 forks source link

KSP: Serde can't work with discrimantor #768

Open altro3 opened 8 months ago

altro3 commented 8 months ago

Get a sample application: https://github.com/altro3/micronaut3-bug/tree/kotlin (branch kotlin)

Send simple request to endpoint localhost:8080/test

{
    "numWings": 2,
    "beakLength": 12.1,
    "featherDescription": "this is description",
    "class": "ave",
    "color": "red"
}

And you will see exception:

изображение

Sample controller:

    @Post("/test")
    open fun test(@Body @NotNull @Valid animal: Animal): Mono<Animal> {
        return Mono.just(animal)
    }

Sample model classes:

@Serdeable
@JsonPropertyOrder(
        Animal.JSON_PROPERTY_PROPERTY_CLASS,
        Animal.JSON_PROPERTY_COLOR
)
@JsonIgnoreProperties(
        value = ["class"], // ignore manually set class, it will be automatically generated by Jackson during serialization
        allowSetters = true // allows the class to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "class", visible = true)
@JsonSubTypes(
        JsonSubTypes.Type(value = Bird::class, name = "ave")
)
open class Animal(
        @Nullable
        @JsonProperty(JSON_PROPERTY_COLOR)
        @JsonInclude(JsonInclude.Include.USE_DEFAULTS)
        open var color: ColorEnum? = null,
        @Nullable
        @JsonProperty(JSON_PROPERTY_PROPERTY_CLASS)
        @JsonInclude(JsonInclude.Include.USE_DEFAULTS)
        open var propertyClass: String? = null,
) {

    companion object {

        const val JSON_PROPERTY_PROPERTY_CLASS = "class"
        const val JSON_PROPERTY_COLOR = "color"
    }
}

Bird

@Serdeable
@JsonPropertyOrder(
        Bird.JSON_PROPERTY_NUM_WINGS,
        Bird.JSON_PROPERTY_BEAK_LENGTH,
        Bird.JSON_PROPERTY_FEATHER_DESCRIPTION
)
data class Bird(
        @Nullable
        @JsonProperty(JSON_PROPERTY_NUM_WINGS)
        @JsonInclude(JsonInclude.Include.USE_DEFAULTS)
        var numWings: Int? = null,
        @Nullable
        @JsonProperty(JSON_PROPERTY_BEAK_LENGTH)
        @JsonInclude(JsonInclude.Include.USE_DEFAULTS)
        var beakLength: BigDecimal? = null,
        @Nullable
        @JsonProperty(JSON_PROPERTY_FEATHER_DESCRIPTION)
        @JsonInclude(JsonInclude.Include.USE_DEFAULTS)
        var featherDescription: String? = null,
) : Animal() {

    companion object {

        const val JSON_PROPERTY_NUM_WINGS = "numWings"
        const val JSON_PROPERTY_BEAK_LENGTH = "beakLength"
        const val JSON_PROPERTY_FEATHER_DESCRIPTION = "featherDescription"
    }
}

ColorEnum

@Serdeable
enum class ColorEnum(
        @get:JsonValue val value: String
) {

    @JsonProperty("red")
    RED("red");

    override fun toString(): String {
        return value
    }

    companion object {

        @JvmField
        val VALUE_MAPPING = entries.associateBy { it.value }

        @JsonCreator
        @JvmStatic
        fun fromValue(value: String): ColorEnum {
            require(VALUE_MAPPING.containsKey(value)) { "Unexpected value '$value'" }
            return VALUE_MAPPING[value]!!
        }
    }
}

Exception reproduce only with KSP.

IMPRTANT: For this problem it doesn' matter do you use field: prefix or not. It doesn't work in both cases

Example Application

https://github.com/altro3/micronaut3-bug/tree/kotlin

Version

4.3.5

altro3 commented 8 months ago

@dstepanov next bug from micronaut-openapi :-)

altro3 commented 8 months ago

@dstepanov problem only with KSP, with java and KAPT all works fine. You can check java in the same scenario and compare introspection in the same project in branch main: https://github.com/altro3/micronaut3-bug/tree/main

altro3 commented 8 months ago

Also can say that with io.micronaut:micronaut-jackson-databind all works fine

altro3 commented 7 months ago

@dstepanov You close it too early. Still have the same error: изображение

altro3 commented 7 months ago

@dstepanov Please, reopen issue. It still doesn't work :-(

altro3 commented 6 months ago

@dstepanov Hi! Any news about this?

dstepanov commented 6 months ago

Nope, need to check again

dstepanov commented 5 months ago

@altro3 Looks like @field:JsonProperty(JSON_PROPERTY_PROPERTY_CLASS) is needed

altro3 commented 5 months ago

@dstepanov No, problem not here. I try to generate classes with prefix field, and now I see wrong value in propertyClass field:

изображение

As you see, discriminator value must be ave, but we see className in this field - Bird.

I tried write test for KSP here - https://github.com/micronaut-projects/micronaut-serialization/pull/833 , but every time I see problem with not found Introspected class, but in temp directory I see it.

In my opinion, you need to write a test for this case and understand what the problem is with KSP.

@dstepanov But without your help I won't be able to do this

altro3 commented 5 months ago

@dstepanov Interesting difference between introspection for KSP and KAPT (left is KSP):

изображение

Intropected class (the same for KAP and KSP):

@Serdeable
@JsonPropertyOrder(
        Animal.JSON_PROPERTY_PROPERTY_CLASS,
        Animal.JSON_PROPERTY_COLOR
)
@Generated("io.micronaut.openapi.generator.KotlinMicronautServerCodegen")
@JsonIgnoreProperties(
        value = ["class"], // ignore manually set class, it will be automatically generated by Jackson during serialization
        allowSetters = true // allows the class to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "class", visible = true)
@JsonSubTypes(
        JsonSubTypes.Type(value = Bird::class, name = "ave"),
        JsonSubTypes.Type(value = Mammal::class, name = "mammalia"),
        JsonSubTypes.Type(value = Reptile::class, name = "reptilia")
)
open class Animal(
    @field:Nullable
    @field:Schema(name = "color", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @field:JsonProperty(JSON_PROPERTY_COLOR)
    @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS)
    open var color: ColorEnum? = null,
    @field:Nullable
    @field:Schema(name = "class", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @field:JsonProperty(JSON_PROPERTY_PROPERTY_CLASS)
    @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS)
    open var propertyClass: String? = null,
) {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as Animal

        if (propertyClass != other.propertyClass) return false
        if (color != other.color) return false

        return true
    }

    override fun hashCode(): Int {
        var result = propertyClass?.hashCode() ?: 0
        result = 31 * result + (color?.hashCode() ?: 0)
        return result
    }

    override fun toString(): String {
        return "Animal(propertyClass='$propertyClass', color='$color')"
    }

    companion object {

        const val JSON_PROPERTY_PROPERTY_CLASS = "class"
        const val JSON_PROPERTY_COLOR = "color"
    }
}
altro3 commented 5 months ago

It became clear why if we add the field prefix with KSP, we have the wrong Bird value. The problem is in object serialization when requested: изображение

As you see, object Bird with serialization set value Bird to class property. Must be ave My test Birdobject is:

изображение

As you see, I don't set field propertyClass. This value calculated by micronaut-serde

altro3 commented 5 months ago

Problem in class SerBean:

изображение

As you see, you set value for decriptor annotaion value SerdeConfig.TYPE_NAME. SerdeConfig.TYPE_NAME annotaion value is a class name. I don't know why. Maybe problem is before and we have the wrong value in tis annotaion value.

I am 100% sure that we need a test, because there are at least 2 bugs for KSP