Closed kchinburarat closed 1 month ago
Hi,
There are a three things i spotted - assuming you want to use the built-in "handleSchemaAnnotations()"-step to change the type of the swagger-schema (though this is still a bit of a tricky and limited feature)
private inline fun <reified T> createDefaultPrimitiveTypeData(): PrimitiveTypeData {
return PrimitiveTypeData(
id = TypeId.build(T::class.qualifiedName!!),
simpleName = T::class.simpleName!!,
qualifiedName = T::class.qualifiedName!!,
annotations = mutableListOf(
AnnotationData(
name = SwaggerTypeHint::class.qualifiedName!!, // use the @SwaggerTypeHint-annotation
values = mutableMapOf(
"type" to "date" // specify the type
),
annotation = null
)
)
)
}
customProcessor<LocalDate>
it looks for one with the same name as the qualified name of the LocalDate-class (= "java.time.LocalDate"). The "LocalDateSerializer" however changes the name of the serial descriptor to just "LocalDate" (here: PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
), no longer matching the one for the custom processor. You would either rename the serial descriptor in the LocalDateSerializer or specify the name for the custom processor as customProcessor("LocalDate") {
.With these two changes the result would be this ...
{
"root" : {
"$ref" : "#/components/schemas/io.github.smiley4.schemakenerator.test.Test.Companion.ClassWithLocalDate",
"exampleSetFlag" : false
},
"componentSchemas" : {
"io.github.smiley4.schemakenerator.test.Test.Companion.ClassWithLocalDate" : {
"title" : "ClassWithLocalDate",
"required" : [ "DoB", "DoBWithTime" ],
"type" : "object",
"properties" : {
"DoB" : {
"title" : "String",
"type" : "date",
"exampleSetFlag" : false
},
"DoBWithTime" : {
"title" : "String",
"type" : "date",
"exampleSetFlag" : false
}
},
"exampleSetFlag" : false
}
}
}
createDefaultPrimitiveTypeData< String >(mutableMapOf(
resulting in duplicate ids here id = TypeId.build(T::class.qualifiedName!!),
. Since ids are treated as unique, one of them gets dropped....without the "format". Adding this would currently be a bit more tricky, since its not supported in the "SwaggerTypeHint"-annotation (maybe i should change that :thinking: )
Alternative
Another option to support also the "format" property would be to write your own "marker" annotation together with a custom step.
Creating the primitive type, but adding a "custom" annotation e.g. with name "swagger_type_and_format" and the required type & format information:
private inline fun <reified T> createDefaultPrimitiveTypeData(format: String): PrimitiveTypeData {
return PrimitiveTypeData(
id = TypeId.build(T::class.qualifiedName!!),
simpleName = T::class.simpleName!!,
qualifiedName = T::class.qualifiedName!!,
annotations = mutableListOf(
AnnotationData(
name = "type_format_annotation",
values = mutableMapOf(
"type" to "date",
"format" to format
),
annotation = null
)
)
)
}
Replace the "handleSchemaAnnotations()" with your own one handling the custom annotation and modifying the swagger schema:
.customizeTypes { typeData, typeSchema ->
typeData.annotations.find { it.name == "type_format_annotation" }?.also { annotation ->
typeSchema.format = annotation.values["format"]?.toString()
typeSchema.type = annotation.values["type"]?.toString()
}
}
Make sure the TypeIds don't collide by e.g. calling the create function like this: createDefaultPrimitiveTypeData<LocalDateTime>
and createDefaultPrimitiveTypeData<LocalDate>
This would then produce:
{
"root" : {
"$ref" : "#/components/schemas/io.github.smiley4.schemakenerator.test.Test.Companion.ClassWithLocalDate",
"exampleSetFlag" : false
},
"componentSchemas" : {
"io.github.smiley4.schemakenerator.test.Test.Companion.ClassWithLocalDate" : {
"title" : "ClassWithLocalDate",
"required" : [ "DoB", "DoBWithTime" ],
"type" : "object",
"properties" : {
"DoB" : {
"title" : "LocalDate",
"type" : "date",
"format" : "yyyy-MM-dd",
"exampleSetFlag" : false
},
"DoBWithTime" : {
"title" : "LocalDateTime",
"type" : "date",
"format" : "date-time",
"exampleSetFlag" : false
}
},
"exampleSetFlag" : false
}
}
}
I hope i could help. I know this is still a bit weird to use and i'm still thinking how to best support this use case. If you have any more questions or need more of the example source just let me know :)
@SMILEY4, after applying your suggestion to my POC project, it seems to be working perfectly.
Thanks for the incredibly fast support!
Below is the finalized code.
// Data class
@Serializable
class ClassWithLocalDate(
@Serializable(with = LocalDateSerializer::class)
@SerialName("DoB")
@Example("2024-10-07")
val dateOfBirth: LocalDate,
@SerialName("DoBWithTime")
@Serializable(with = LocalDateTimeSerializer::class)
@Example("2024-10-07")
val dateOfBirthWithTime: LocalDateTime
)
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = LocalDateTime::class)
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.format(formatter))
}
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString(), formatter)
}
}
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) {
val string = value.format(DateTimeFormatter.ISO_DATE)
encoder.encodeString(string)
}
override fun deserialize(decoder: Decoder): LocalDate {
val string = decoder.decodeString()
return LocalDate.parse(string)
}
}
// plugin instalation
install(SwaggerUI) {
info {
title = "Example API"
}
schemas {
// replace default schema-generator with customized one
generator = { type ->
type
.processKotlinxSerialization {
customProcessor<LocalDateTime> {
createDefaultPrimitiveTypeData<LocalDateTime>()
}
customProcessor("LocalDate") {
createDefaultPrimitiveTypeData<LocalDate>()
}
}
.connectSubTypes()
.generateSwaggerSchema()
.withTitle(TitleType.SIMPLE)
.handleCoreAnnotations()
//.handleSwaggerAnnotations()
.customizeTypes { typeData, typeSchema ->
typeData.annotations.find { it.name == "type_format_annotation" }?.also { annotation ->
typeSchema.format = annotation.values["format"]?.toString()
typeSchema.type = annotation.values["type"]?.toString()
}
}
.handleSchemaAnnotations()
.compileReferencingRoot()
}
}
}
/***
* Issue : https://github.com/SMILEY4/ktor-swagger-ui/issues/135
*/
private inline fun <reified T> createDefaultPrimitiveTypeData(format: String = "date"): PrimitiveTypeData {
return PrimitiveTypeData(
id = TypeId.build(T::class.qualifiedName!!),
simpleName = T::class.simpleName!!,
qualifiedName = T::class.qualifiedName!!,
annotations = mutableListOf(
AnnotationData(
name = "type_format_annotation",
values = mutableMapOf(
"type" to "string", // specify the type
"format" to format // specify the format
),
annotation = null
)
)
)
}
// Routing
get("/ClassWithLocalDate" , {
response {
// information about a "200 OK" response
code(HttpStatusCode.OK) {
// body of the response
body<ClassWithLocalDate>{
description = "Class with LocalDate"
example("DOg"){
ClassWithLocalDate(
dateOfBirth = LocalDate.now() ,
dateOfBirthWithTime = LocalDateTime.now()
)
}
}
}
code(HttpStatusCode.NotFound) {
description = "the pet with the given id was not found"
}
}
}) {
call.respond(
ClassWithLocalDate(
dateOfBirth = java.time.LocalDate.now() ,
dateOfBirthWithTime = java.time.LocalDateTime.now()
)
)
}
I have a proof-of-concept project with a data class like the one below. Note: ktor-swagger-ui :
3.4.0
schema_kenerator:1.4.0
then i try to configure swagger option like
I expect the api.json to set the type to string, but it is currently null.
I'm not sure if I've missed something!
I would like to set type as
string
and format asdate-time
Could you help :)