swagger-core
swagger-core copied to clipboard
Sealed kotlin class with oneOf annotation generates a type: object
Describe the bug
Given a Kotlin sealed class annotated with @Schema(oneOf = ..), swagger-core resolves a schema which contains both type: object and oneOf
To Reproduce Model classes:
@Schema(
oneOf = [AnyShape.Square::class, AnyShape.Circle::class],
discriminatorProperty = "type"
)
sealed class AnyShape {
data class Square(val size: Float) : AnyShape() {
val type = ShapeType.Square
}
data class Circle(val radius: Float) : AnyShape() {
val type = ShapeType.Circle
}
}
@Schema(enumAsRef = true)
enum class ShapeType { Square, Circle }
Unit test to reproduce:
@Test
fun `oneOf annotation generates a oneOf schema`() {
val resolvedSchema: ResolvedSchema = ModelConverters.getInstance()
.resolveAsResolvedSchema(AnnotatedType(AnyShape::class.java))
assertNotEquals("object", resolvedSchema.schema.type)
val composed = resolvedSchema.schema as ComposedSchema
assertEquals(composed.oneOf.size, 2)
}
The issue with the both oneOf and type: object being present is that other swagger libraries have to make the assumption as to how to parse such a schema.
See my other ticket https://github.com/yonaskolb/SwagGen/issues/302
I wonder if the type should be simply omitted for composed schemes here https://github.com/swagger-api/swagger-core/blob/0a16eb7c9be4475e90957e3b91b6e03ace5124f6/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java#L491-L494
@JanC any update on this? Did you figure out any workaround?
hey, nope :) I gave up on trying. It seems Kotlin sealed classes and openapi generation (or the other way round) is a bit problematic.
@JanC I tried the following and kinda got the result I believe you need:
@GetMapping("/test")
@ApiResponses(value = [
ApiResponse(responseCode = "200", description = "OK", content = [
(Content(mediaType = "application/json", schema = Schema(implementation = Property::class)))
])])
fun test() = Property(Attributes.TypeA("title"))
data class Property(val attributes: Attributes)
@Schema(name = "Property.Attributes", oneOf = [Attributes.TypeA::class, Attributes.TypeB::class])
sealed interface Attributes {
data class TypeA(val title: String): Attributes
data class TypeB(val name: String): Attributes
}
Not sure if the difference is that I am using a sealed interface and you a sealed class, but you could try
Hi @lluistfc,
It looks like, Kotlin sealed interface and also sealed class from the Swagger perspective works the same.
However, the issue is that the crucial "discriminator" property is not present in generated JSON schema, which makes it useless for code gen. See this example:
@Schema(
description = "Root model.",
subTypes = [RootModel.Child1::class, RootModel.Child2::class],
discriminatorProperty = "type", // same as default value in KotlinX Json serialization
)
@Serializable
sealed interface RootModel {
@Serializable
@SerialName("Child1")
@Schema(description = "Child1")
data class Child1(
val prop1: String,
) : RootModel
@Serializable
@SerialName("Child2")
@Schema(description = "Child2")
data class Child2(
val prop1: String,
) : RootModel
}
Please can someone direct us to a working example of Kotlin sealed interface / class (or anything) using KotlinX Serialization (so the type property is actually not present in root model) with working discriminator property generated in JSON Schema?
Thanks
@Matej-Hlatky I think this works. I'm using jackson for serialisation, not sure if that helps you, but swagger-core uses jackson so I think its the easiest way.
//tell jackson how to serialise and deserialise with discriminator included
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes(
JsonSubTypes.Type(name = "a", value = DiscriminatedOptions.OptionsA::class),
JsonSubTypes.Type(name = "b", value = DiscriminatedOptions.OptionsB::class),
)
//configure the json schema to use a discriminated oneof with the same mapping as we told jackson to use
@Schema(
required = false,
oneOf = [
DiscriminatedOptions.OptionsA::class,
DiscriminatedOptions.OptionsB::class,
],
//important that these match the subtype names in @JsonSubTypes
discriminatorMapping = [
DiscriminatorMapping(value = "a", schema = DiscriminatedOptions.OptionsA::class),
DiscriminatorMapping(value = "b", schema = DiscriminatedOptions.OptionsB::class),
])
sealed interface DiscriminatedOptions {
data class OptionsA(val property1: String) : DiscriminatedOptions
data class OptionsB(val property2: String): DiscriminatedOptions
}