I have the following endpoint:
val myEndpoint: PublicEndpoint[Unit, ErrorResponse, Entity, Any] = endpoint.get
.in("test")
.out(jsonBody[Entity])
.errorOut(jsonBody[ErrorResponse])
Where Entity
and ErrorResponse
are:
sealed trait ErrorResponse
object ErrorResponse {
final case class NotFound(id: String) extends ErrorResponse
final case class UnknownError(error: String) extends ErrorResponse
implicit val notFoundSchema: Schema[NotFound] = Schema.derived
implicit val unknownErrorSchema: Schema[UnknownError] = Schema.derived
implicit val errorResponseSchema: Schema[ErrorResponse] = Schema.derived
}
Then I convert the endpoints to akka routes:
val myEndpointRoute: Route = AkkaHttpServerInterpreter().toRoute(myEndpoint.serverLogic { _ =>
val result: Either[ErrorResponse, Entity] = Right(Entity("some data of the entity"))
Future.successful(result)
})
val swaggerEndpoint = SwaggerInterpreter().fromEndpoints[Future](List(myEndpoint), "Tapir Demo", "1.0")
val swaggerRoutes: Route = AkkaHttpServerInterpreter().toRoute(swaggerEndpoint)
And run the server:
Http()
.newServerAt("localhost", 8080)
.bind(myEndpointRoute ~ swaggerRoutes)
.onComplete {
case Success(_) =>
println(s"Started on port 8080")
case Failure(e) =>
println("Failed to start ... ", e)
}
The issue I have is when browsing the schema for ErrorResponse
I see an hierarchy of objects (#0
and #1
) instead of subtypes like NotFound
, UnknownError
.
How should I define the schema for ErrorResponse
?
PS: dependencies:
"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.9.6",
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.9.6"
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.9.6",
"com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "1.9.6"
That is how swagger UI renders oneOf in OpenAPI Spec 3.1.0, which is the one that tapir uses as the default version. If you change it to OpenAPI Spec 3.0.3 or any 3.x.x you will get the result you expect. Here you have a POC that reproduce your case (I used pekko-http instead of akka-http, but it's the same)
lazy val root = (project in file("."))
.settings(
name := "tapir-swagger-ui-poc",
libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.4.14",
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
"org.apache.pekko" %% "pekko-actor-typed" % "1.0.2",
"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % "1.9.6",
"com.softwaremill.sttp.tapir" %% "tapir-sttp-stub-server" % "1.9.6",
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "1.9.6",
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.9.6",
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % "1.9.6",
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.7.3"
),
run / fork := true
)
import com.typesafe.scalalogging.Logger
import io.circe.generic.auto._
import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.server.Route
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
import sttp.tapir.swagger.SwaggerUIOptions
import sttp.tapir.swagger.bundle.SwaggerInterpreter
import scala.concurrent.{ExecutionContextExecutor, Future}
object Main {
sealed trait ErrorResponse
object ErrorResponse {
final case class NotFound(id: String) extends ErrorResponse
final case class UnknownError(error: String) extends ErrorResponse
implicit val notFoundSchema: Schema[NotFound] = Schema.derived
implicit val unknownErrorSchema: Schema[UnknownError] = Schema.derived
implicit val errorResponseSchema: Schema[ErrorResponse] = Schema.derived
}
private val myEndpoint = endpoint.get
.in("test")
.out(stringBody)
.errorOut(jsonBody[ErrorResponse])
def main(args: Array[String]): Unit = {
val logger = Logger(getClass)
implicit val system: ActorSystem[Nothing] =
ActorSystem(Behaviors.empty, "money-maniacs-http")
implicit val executionContext: ExecutionContextExecutor =
system.executionContext
/**
* swagger UI endpoints for OpenAPI Spec 3.0.3
*/
val swaggerEndpoints_3_0_3: List[ServerEndpoint[Any, Future]] =
SwaggerInterpreter(
customiseDocsModel = openAPI => openAPI.openapi("3.0.3"), // set OpenAPI spec version
swaggerUIOptions = SwaggerUIOptions.default.pathPrefix(List("docs-3.0.3")) // set path prefix for docs
)
.fromEndpoints[Future](
List(myEndpoint), // list of endpoints
"My App",
"1.0"
)
/**
* swagger UI endpoints for OpenAPI Spec 3.1.0
*/
val swaggerEndpoints_3_1_0: List[ServerEndpoint[Any, Future]] =
SwaggerInterpreter(
swaggerUIOptions = SwaggerUIOptions.default.pathPrefix(List("docs-3.1.0")) // set path prefix for docs
)
.fromEndpoints[Future](
List(myEndpoint), // list of endpoints
"My App",
"1.0"
)
val routes: Route =
PekkoHttpServerInterpreter()
.toRoute(swaggerEndpoints_3_0_3 ::: swaggerEndpoints_3_1_0)
val interface = "0.0.0.0"
val port = 9000
Http()
.newServerAt(interface = interface, port = port)
.bind(routes)
.foreach { _ =>
logger.info(s"Server started at $interface:$port")
logger.info(s"Press enter to stop the server")
}
}
}
Once you start the server with sbt run
you can check the endpoints for OpenAPI Spec 3.1.0
and 3.0.3
open http://localhost:9000/docs-3.0.3
open http://localhost:9000/docs-3.1.0
A similar issue was reported in Swagger. The suggestion was add the property title with the same name of the object.
For tapir 1.9.6 which is the version you are using and the latest one released up to now, you can get the same result changing the schemas defined to something like
import scala.reflect.runtime.universe.typeOf
implicit val notFoundSchema: Schema[NotFound] =
Schema.derived.title(typeOf[NotFound].typeSymbol.name.toString)
implicit val unknownErrorSchema: Schema[UnknownError] =
Schema.derived.title(typeOf[UnknownError].typeSymbol.name.toString)
implicit val errorResponseSchema: Schema[ErrorResponse] =
Schema.derived.title(typeOf[UnknownError].typeSymbol.name.toString)
doing that, will produce the result you are looking for (the hash with the number stills there but now it also adds the description you want)