softwaremill / tapir

Rapid development of self-documenting APIs

Home Page:https://tapir.softwaremill.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG] Default `CommaSeparated[String]` / `Delimited[",", String]` results in single value rather than list of values (OpenAPI)

jnatten opened this issue · comments

Tapir version: 1.9.11
Scala version: 2.13.13

If i define an endpoint with a query parameter with type CommaSeparated[String] and a default value like Delimited[",", String](List("a", "b", "c")) the actual default value result in openapi default value like this: "default": ["a,b,c"] rather than what i expected: "default": ["a","b","c"]

If the default list is empty the generated doc is "default": [""] which results in our swagger-ui defaulting to a single empty element in list.
I work around it by using Option[CommaSeparated[String]] and defaulting to None, but it isn't perfect 😄

How to reproduce?

Code example with openapi endpoint
import io.circe.syntax.EncoderOps
import sttp.apispec.openapi.{Components, Info}
import sttp.tapir._
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.jdkhttp.{Id, JdkHttpServer, JdkHttpServerOptions}
import sttp.apispec.openapi.circe._
import sttp.tapir.model.{CommaSeparated, Delimited}

import java.util.concurrent.Executors

object Main {
  def main(args: Array[String]): Unit = {
    val endpoints: List[ServerEndpoint[Any, Id]] = List(
      endpoint.get
        .description("Hello this is some endpoint")
        .in("hello")
        .in(query[CommaSeparated[String]]("some-list").default(Delimited[",", String](List("a", "b", "c"))))
        .out(stringBody)
        .serverLogicPure { someList =>
          Right(s"Yay, ${someList.values.map(e => s"'$e'")}!")
        }
    )

    val docs = {
      val info                = Info(title = "testapi", version = "1.0")
      val docs                = OpenAPIDocsInterpreter().serverEndpointsToOpenAPI(endpoints, info)
      val generatedComponents = docs.components.getOrElse(Components.Empty)
      val docsWithComponents  = docs.components(generatedComponents).asJson
      docsWithComponents.asJson
    }

    def swaggerEndpoint: ServerEndpoint[Any, Id] = endpoint.get
      .in("api-docs")
      .out(stringJsonBody)
      .out(header("Access-Control-Allow-Origin", "*"))
      .serverLogicPure { _ => Right(docs.noSpaces) }

    val executor = Executors.newVirtualThreadPerTaskExecutor()
    val opts     = JdkHttpServerOptions.Default
    val server = JdkHttpServer()
      .options(opts)
      .addEndpoints(endpoints :+ swaggerEndpoint)
      .executor(executor)
      .port(8080)
      .start()

    synchronized(wait())
  }
}

I'm surprised you got any default entry in the schema at all - when I tried to reproduce, the default was missing entirely, only after removing some duplicate code in #3584 all the attributes are properly copied.

However, the problem remains - I still get the encoded form in the default entry - which is expected, since all defaults values are always encoded (and the encoded form of the query parameter is a "glued" string). I'll think what we can do about that :)