softwaremill / tapir

Rapid development of self-documenting APIs
https://tapir.softwaremill.com
Apache License 2.0
1.35k stars 411 forks source link

[BUG] The OpenAPIDocsInterpreter does not generate a correct YAML for a Tapir endpoint with oneOf output #3152

Open straszydlo opened 1 year ago

straszydlo commented 1 year ago

Tapir version: 1.7.3

Scala version: 3.3.0 and 2.13.x

Describe the bug The OpenAPIDocsInterpreter does not generate a correct YAML for a Tapir endpoint that uses oneOf in its output.

How to reproduce?

Minimal reproduction:

//> using scala 3.3
//> using lib "com.softwaremill.sttp.tapir::tapir-core:1.7.3"
//> using lib "com.softwaremill.sttp.tapir::tapir-json-circe:1.7.3"
//> using lib "com.softwaremill.sttp.tapir::tapir-openapi-docs:1.7.3"
//> using lib "com.softwaremill.sttp.apispec::openapi-circe-yaml:0.6.0"

import io.circe.generic.auto.*
import sttp.apispec.openapi.OpenAPI
import sttp.apispec.openapi.circe.yaml.*
import sttp.tapir.*
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.json.circe.*
import sttp.tapir.generic.auto.*

object TapirBugExample extends App:
  case class First(value: Int)
  case class Second(value: String)

  val endpointOutput =
    oneOf(
      oneOfVariant(jsonBody[First]),
      oneOfVariant(jsonBody[Second])
    )

  val testEndpoint =
    endpoint
      .in("demo")
      .out(endpointOutput)

  val docs: OpenAPI = OpenAPIDocsInterpreter().toOpenAPI(testEndpoint, "test", "1.0").openapi("3.0.3")

  println(docs.toYaml3_0_3)

This code, when run with scala-cli, produces the following output:

openapi: 3.0.3
info:
  title: test
  version: '1.0'
paths:
  /demo:
    get:
      operationId: getDemo
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Second'
components:
  schemas:
    First:
      required:
      - value
      type: object
      properties:
        value:
          type: integer
          format: int32
    Second:
      required:
      - value
      type: object
      properties:
        value:
          type: string

The First type is missing from the responses section of the /demo path.

adamw commented 1 year ago

I think the docs might not be clear enough, and that's probably something that we should fix. oneOf variants specify alternate entire outputs - including status code, headers and body. In OpenAPI, you can only specify one output per status code. As you've got two output variants with the same (default - 200) status code, they get overwritten, and you only get one.

For example,

  val endpointOutput =
    oneOf[X](
      oneOfVariant(statusCode(StatusCode.Ok).and(jsonBody[First])),
      oneOfVariant(statusCode(StatusCode.Created).and(jsonBody[Second]))
    )

will have two outputs (for different status codes). However, that's probably not what you want - I'm guessing you'd like to have alternate body representations, for the 200 status code. To do that, you'll need to add a common supertype for the case classes, and declare that your output is a json body for that class:

  sealed trait Parent
  case class First(value: Int) extends Parent
  case class Second(value: String) extends Parent

  val endpointOutput = jsonBody[Parent]

This gives the output:

openapi: 3.0.3
info:
  title: test
  version: '1.0'
paths:
  /demo:
    get:
      operationId: getDemo
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Parent'
components:
  schemas:
    First:
      required:
      - value
      type: object
      properties:
        value:
          type: integer
          format: int32
    Parent:
      oneOf:
      - $ref: '#/components/schemas/First'
      - $ref: '#/components/schemas/Second'
    Second:
      required:
      - value
      type: object
      properties:
        value:
          type: string
adamw commented 1 year ago

Here's the docs note: https://github.com/softwaremill/tapir/commit/ea3e94ba0e5fea8d291502af5f9eb8f6ed1e5220