Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying format of parameter in request #168

Closed
king-phyte opened this issue Dec 28, 2024 · 14 comments
Closed

Allow specifying format of parameter in request #168

king-phyte opened this issue Dec 28, 2024 · 14 comments

Comments

@king-phyte
Copy link

Hello. First of all, thank you for the excellent work you put into this well thought out and executed package.

I am using ktor-swagger-ui:4.1.2. I want a way to specify the format of a path parameter in the request section of the route documentation. See the sample below:

object UUIDSerializer : KSerializer<UUID> {
    override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): UUID {
        return UUID.fromString(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: UUID) {
        encoder.encodeString(value.toString())
    }
}

// Using Type-safe routing (https://ktor.io/docs/server-resources.html)
@Resource("/users")
class Users() {
    @Resource("{id}") // <--- Trying to document this parameter
    class Id(
        val parent: Users = Users(),
        @Serializable(with = UUIDSerializer::class)
        @Schema(format = "uuid") // <--- This is not recognised in Type-safe routing
        @Format("uuid") // Not recognised
        val id: UUID,
    )
}

get<Users.Id>({
    request {
        pathParameter<UUID>("id") {
            description = "ID of the 
            format = "uuid" // <--- This is not available, but it is allowed in the OpenAPI specs
        }
    }
}) { user ->
    call.respond(status = HttpStatusCode.OK, message = mapOf("id" to user.id))
}

This generates the following (some fields are removed for brevity)

"/users/{id}" : {
  "get" : {
    "parameters" : [ {
      "name" : "id",
      "in" : "path",
      "required" : true,
      "schema" : {
        "type" : "string",
        "title" : "String"
        "format": "uuid" // <--- this is missing
      }
    } ],
}

If you need any additional information, let me know and I will do my best to provide it. Thank you.

@ddenchev
Copy link

I am using a custom processor and would also love the ability to set the "format".

schemas {
    generator = { type ->
        type
            .processKotlinxSerialization {
                customProcessor<Uuid> {
                    PrimitiveTypeData( ... )
                }    

i am pretty new to the library, but it feels ergonomic to be able to set the format through the custom processor override.

@king-phyte
Copy link
Author

@ddenchev I was using a custom processor as well, but there was a deprecation somewhere in there. Did you hit it too? If you did, how did you proceed? For now, I am using type redirection so I need formatting to get help from Swagger

schemas {
            generator = { type ->
                type
                    .collectJacksonSubTypes({ it.processReflection() })
                    .processReflection {
                        redirect<OffsetDateTime, String>()
                        redirect<UUID, String>()
                    }
            }
        }

@ddenchev
Copy link

@king-phyte
This is what my config currently looks like (I am using kotlinx serialization):

.processKotlinxSerialization {
	customProcessor<Uuid> {
		PrimitiveTypeData(
			id = TypeId(
				String::class.qualifiedName!!,
				emptyList(),
				"uuid"
			),
			simpleName = String::class.simpleName!!,
			qualifiedName = String::class.qualifiedName!!
		)
	}
}

As I said, I am new to the library so not sure about all the customization options and what rough edges are me not knowing about it.

The generated schema looks like this:


  "schema" : {
	"type" : "string",
	"title" : "String#uuid"
  },

In redoc, that looks like this:
image

Instead of like this:
image

@king-phyte
Copy link
Author

@ddenchev Thank you for sharing that. It looks like we have hit the same wall.

@king-phyte
Copy link
Author

@SMILEY4 Can you please take a look at this?

@SMILEY4
Copy link
Owner

SMILEY4 commented Jan 5, 2025

Hi @king-phyte, @ddenchev,

I'm sorry for the confusion, i'm still trying to figure out how to make this simpler or document it better :)

For a bit of context:

Generating a schema for a kotlin type happens (very rougly) in two independent steps:

  1. collecting all the data for the type
  2. generating a swagger schema from the collected data

The general idea for the uuids here would be:

  • to use a custom processor for the uuid type to not use the default "data-collection" behavior in step 1 but to overwrite it with custom data where we can then specify anything about the type we want, i.e. a primitive type to be understood by step 2 as "string"
  • step 2 (see handleCoreAnnotations()) already automatically looks at annotations like @Format and generates the swagger schema accordingly. The annotation can either come from manually adding it to fields or by specifying it as an annotation on the type itself in the custom processor from step 1.

Assuming this model with the uuid field and the UUIDSerializer:

@Serializable
class Id(
    @Serializable(with = UUIDSerializer::class)
    val id: UUID, // no need for @Format("uuid") here. This can just be handled in the custom processor that is required anyway here.
)

object UUIDSerializer : KSerializer<UUID> {
    override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): UUID {
        return UUID.fromString(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: UUID) {
        encoder.encodeString(value.toString())
    }
}

When using reflection, the schema generator config could look something like this (sticking close the the default plugin config here):

install(SwaggerUI) {
    schemas {
        generator = { type ->
            type
            	.collectSubTypes()
                .processReflection {
                    // add a custom processor for type "UUID"
                    customProcessor<UUID> {
                        PrimitiveTypeData(
                            // needs a (unique) id for our type, overwriting UUID so taking that
                            id = TypeId.build(UUID::class.qualifiedName!!),
                            // qualified name must be the one of "String".
                            // The generator currently looks at the qualified name to
                            // determine the type of the swagger schema :/
                            qualifiedName = String::class.qualifiedName!!,
                            // simple name can be anything.
                            // Using uuid here since this will become
                            // the "title" in the swagger schema.
                            simpleName = UUID::class.simpleName!!,
                            annotations = mutableListOf(AnnotationData(
                                // adding the @Format annotation to our custom type here.
                                // This way it does not need to be added on every field in the models.
                                name = Format::class.qualifiedName!!,
                                values = mutableMapOf("format" to "uuid") // with value "uuid"
                            ))
                        )
                    }
                }
                .connectSubTypes()
                .handleNameAnnotation()
                .generateSwaggerSchema()
                 // @Format annotations will be handled in this step and is set in the swagger schema
                .handleCoreAnnotations()
                .withTitle(TitleType.SIMPLE)
                .compileReferencingRoot()
        }
    }
}

For kotlinx it's (almost) the same:

install(SwaggerUI) {
    schemas {
        generator = { type ->
            type
                .processKotlinxSerialization {
                    // register the custom processor the name matching the
                    // one defined in the custom KSerializer here:
                    //     PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
                    customProcessor("UUID") {
                        PrimitiveTypeData(
                            id = TypeId.build(UUID::class.qualifiedName!!),
                            qualifiedName = String::class.qualifiedName!!,
                            simpleName = UUID::class.simpleName!!,
                            annotations = mutableListOf(AnnotationData(
                                name = Format::class.qualifiedName!!,
                                values = mutableMapOf("format" to "uuid")
                            ))
                        )
                    }
                }
                .generateSwaggerSchema()
                 // set "format" in swagger schema according to @Format annotation
                .handleCoreAnnotations()
                .withTitle(SIMPLE)
                .compileReferencingRoot()
        }
    }
}

I hope this helps and clears up some of the confusion. If you have any open questions, please feel free to ask.

@king-phyte
Copy link
Author

@SMILEY4 Thank you for the response.

It indeed works if I proceed with it like you described above. I was using custom processors by following the docs/examples, but as I said in my comment above, I hit some deprecation warnings so I gave up and switched to redirection.
Perhaps you can update the examples with this.

Also, please evaluate the attached pull request, and decide if it is necessary or not (maybe for one-off moments of wanting to specify format), since the method you described here works fine if we always want to override the type.

Thank you once again!

@SMILEY4
Copy link
Owner

SMILEY4 commented Jan 5, 2025

I hit some deprecation warnings so I gave up and switched to redirection. Perhaps you can update the examples with this.

I updated the ktor-swagger-ui and schema-kenerator wiki and replaced mentions of the deprecated withAutoTitle with the updated withTitle functions. That should be the one you encountered ?

Also, please evaluate the attached pull request, and decide if it is necessary or not

I don't think we need it since we have the custom processor that is required for larger modifications and the @Format annotation should imo suffice and work out of the box for the rest. I also would prefer to keep all the schema stuff in the schema-kenerator project.
Thank you for your pr/suggestion anyway!

@king-phyte
Copy link
Author

I updated the ktor-swagger-ui and schema-kenerator wiki and replaced mentions of the deprecated withAutoTitle with the updated withTitle functions. That should be the one you encountered ?

I don't remember which one exactly I encountered, but it was somewhere inside the PrimitiveTypeData

I don't think we need it since we have the custom processor that is required for larger modifications and the @Format annotation should imo suffice and work out of the box for the rest. I also would prefer to keep all the schema stuff in the schema-kenerator project.

Thank you for your pr/suggestion anyway!

Alright then. You wouldn't mind if I closed this issue (and the PR), yes?

@SMILEY4
Copy link
Owner

SMILEY4 commented Jan 5, 2025

I don't remember which one exactly I encountered, but it was somewhere inside the PrimitiveTypeData

thanks, i'll have another look.

From my side this issue and the pr can be closed 👍

@king-phyte
Copy link
Author

Alright. Thank you yet once more for everything!

@king-phyte
Copy link
Author

@SMILEY4 whilst I have you here, if you don't mind me asking, do you have plans of adding support for ReDoc?

@SMILEY4
Copy link
Owner

SMILEY4 commented Jan 5, 2025

not until now, though i quickly looked into it and it seems quite simple to implement.
The bigger problem might be how to add redoc to a project called ktor-swagger-ui 🤔

@king-phyte
Copy link
Author

Believe me when I say I have thought about that a lot 😅. But I figured having the functionalty first then worry about the naming later is a better approach

I considered forking the repo and maintaining a separate package for redoc, but I thought that would be a bit overboard, considering ReDoc consumes OpenAPI almost the same way as Swagger UI does and this package already does 99% of that work

What we need may be adding

install(SwaggerUI){
    redoc {...}
}

and

routing {
    route("docs/redoc") {
                redoc("/openapi.json")
            }
}

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 a pull request may close this issue.

3 participants