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

Proposal - Support Multipart request data via a new annotation #45

Open
schott12521 opened this issue Feb 5, 2025 · 2 comments
Open

Comments

@schott12521
Copy link
Contributor

schott12521 commented Feb 5, 2025

Have you ever had to work with multipart data uploads? When used with resource based routing, the route looks like:

post { _: VideoResource.Upload, multipart: MultiPartData ->

Unfortunately this generates a scheme reference in the openapi spec and it doesn't really work as expected. Which is fine, because it doesn't seem like ktor-docs advertises multipart support.

Here is what is generated:

          application/json:
            schema:
              $ref: "#/components/schemas/io.ktor.http.content.MultiPartData"

but it should be something like:

      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                description:
                  type: string
                  description: Video description
                video:
                  type: string
                  format: binary
                  description: Video file to upload
            encoding:
              video:
                contentType: video/mov, video/quicktime, video/gif, image/gif, video/webm

I think the best way to support this is probably via an annotation, because its additional information outside of the ktor request (contentType is at least, so we might as well take it all in via annotation). It would be nice if we could utilize @KtorFieldDescription for the properties in the multipart upload, but it would require a small refactor which would increase its responsibilities.

OpenAPI reference - https://swagger.io/docs/specification/v3_0/describing-request-body/multipart-requests/
Ktor reference - https://ktor.io/docs/server-requests.html#form_data

@schott12521
Copy link
Contributor Author

For anyone that wants to get around this in the meantime, here's what I'm doing:

tasks.register("fixOpenApiSpec") {
    doLast {
        val openApiFile = file("${projectDir}/build/resources/main/openapi/openapi.yaml")

        if (openApiFile.exists()) {
            var specContent = openApiFile.readText()

            val regex =
                """application/json:\s*\n\s*schema:\s*\n\s*\${'$'}ref:\s*"#/components/schemas/io.ktor.http.content.MultiPartData"""".toRegex()

            val newString =
                """multipart/form-data:
            schema:
              type: object
              properties:
                description:
                  type: string
                  description: Video description
                video:
                  type: string
                  format: binary
                  description: Video file to upload
            encoding:
              video:
                contentType: video/mov, video/quicktime, video/gif, image/gif, video/webm
            """

            println("🔍 Replacing OpenAPI schema...")

            if (regex.containsMatchIn(specContent)) {
                specContent = specContent.replace(regex, newString)
                openApiFile.writeText(specContent)
                println("✅ OpenAPI spec modified successfully!")
            } else {
                println("⚠️ Match not found! The schema format might be different than expected.")
            }
        } else {
            println("⚠️ OpenAPI spec file not found at ${openApiFile.path}")
        }
    }
}

// Ensure this task runs after swagger documentation generation
tasks.named("compileKotlin") {
    finalizedBy("fixOpenApiSpec")
}

@schott12521
Copy link
Contributor Author

Copying @tabilzad 's response from slack:

yeah i guess something like this should be sufficient. We'd just have to make sure regular type interpretation won't run for Multipart endpoints.

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.EXPRESSION)
annotation class MultiPartSchema(
    val formParts: Array<FormPart> = [],
    val fileParts: Array<FilePart> = []
)

@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class FormPart(
    val name: String,
    val type: KClass<*>,
    val description: String = "",
    val required: Boolean = false,
    val format: String = ""
)

@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class FilePart(
    val name: String,
    val encodingContentTypes: Array<String>,
    val description: String = "",
    val required: Boolean = false,
)

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

No branches or pull requests

1 participant