英文:
Redoc documentation for tapir endpoint with sealed heirarchy not rendering as expected
问题
我正在尝试定义一个Tapir端点,该端点将接受两种不同的潜在负载(在下面的代码片段中,两种不同的定义"Thing"的方式)。我大体上遵循这里的说明:https://circe.github.io/circe/codecs/adt.html,并定义我的端点:
endpoint
.post
.in(jsonBody[ThingSpec].description("Specification of the thing"))
.out(jsonBody[Thing].description("Thing!"))
ThingSpec
是一个密封特质,表示可能负载的两个类都扩展了它:
import io.circe.{Decoder, Encoder, derivation}
import io.circe.derivation.{deriveDecoder, deriveEncoder}
import sttp.tapir.Schema
import sttp.tapir.Schema.annotations.description
import sttp.tapir.generic.Configuration
import cats.syntax.functor._
import io.circe.syntax.EncoderOps
sealed trait ThingSpec {
def kind: String
}
object ThingSpec {
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
implicit val thingConfigDecoder: Decoder[ThingSpec] = Decoder[ThingOneSpec].widen or Decoder[ThingTwoSpec].widen
implicit val thingConfigEncoder: Encoder[ThingSpec] = {
case one @ ThingOneSpec(_, _) => one.asJson
case two @ ThingTwoSpec(_, _) => two.asJson
}
implicit val thingConfigSchema: Schema[ThingSpec] =
Schema.oneOfUsingField[ThingSpec, String](_.kind, _.toString)(
"one" -> ThingOneSpec.thingConfigSchema,
"two" -> ThingTwoSpec.thingConfigSchema
)
}
case class ThingOneSpec(
name: String,
age: Long
) extends ThingSpec {
def kind: String = "one"
}
object ThingOneSpec {
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
implicit val thingConfigEncoder: Encoder[ThingOneSpec] = deriveEncoder(derivation.renaming.snakeCase)
implicit val thingConfigDecoder: Decoder[ThingOneSpec] = deriveDecoder(derivation.renaming.snakeCase)
implicit val thingConfigSchema: Schema[ThingOneSpec] = Schema.derived
}
case class ThingTwoSpec(
height: Long,
weight: Long,
) extends ThingSpec {
def kind: String = "two"
}
object ThingTwoSpec {
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
implicit val thingConfigEncoder: Encoder[ThingTwoSpec] = deriveEncoder(derivation.renaming.snakeCase)
implicit val thingConfigDecoder: Decoder[ThingTwoSpec] = deriveDecoder(derivation.renaming.snakeCase)
implicit val thingConfigSchema: Schema[ThingTwoSpec] = Schema.derived
}
这似乎正常工作,除了生成的Redoc文档中的"请求体部分"。我相信这部分是由以下生成的:
.in(jsonBody[ThingSpec].description("Specification of the thing"))
它只包括ThingOneSpec对象的详细信息,没有提到ThingTwoSpec。"负载"示例部分包括两者。
我的主要问题是如何让文档的"请求体部分"显示两种可能的负载。
然而,我意识到可能没有以最佳方式(从circe/tapir的角度)来完成这个任务。理想情况下,我不想在特质/类中包含显式的识别器(kind
),因为我不希望将其暴露给文档的"负载"部分的最终用户。尽管我已阅读以下文档:
- https://tapir.softwaremill.com/en/v0.17.7/endpoint/customtypes.html
- https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/custom_types/SealedTraitWithDiscriminator.scala
- https://github.com/softwaremill/tapir/issues/315
但我无法在没有显式识别器的情况下使其正常工作。
英文:
I'm trying to define a tapir endpoint, which will accept two potential different payloads (in the snippet below, two different ways of defining a Thing). I'm broadly following the instructions here: https://circe.github.io/circe/codecs/adt.html, and defining my endpoint:
endpoint
.post
.in(jsonBody[ThingSpec].description("Specification of the thing"))
.out(jsonBody[Thing].description("Thing!"))
ThingSpec
is a sealed trait, which both the classes representing possible payloads extend:
import io.circe.{Decoder, Encoder, derivation}
import io.circe.derivation.{deriveDecoder, deriveEncoder}
import sttp.tapir.Schema
import sttp.tapir.Schema.annotations.description
import sttp.tapir.generic.Configuration
import cats.syntax.functor._
import io.circe.syntax.EncoderOps
sealed trait ThingSpec {
def kind: String
}
object ThingSpec {
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
implicit val thingConfigDecoder
: Decoder[ThingSpec] = Decoder[ThingOneSpec].widen or Decoder[ThingTwoSpec].widen
implicit val thingConfigEncoder: Encoder[ThingSpec] = {
case one @ ThingOneSpec(_, _) => one.asJson
case two @ ThingTwoSpec(_, _) => two.asJson
}
implicit val thingConfigSchema: Schema[ThingSpec] =
Schema.oneOfUsingField[ThingSpec, String](_.kind, _.toString)(
"one" -> ThingOneSpec.thingConfigSchema,
"two" -> ThingTwoSpec.thingConfigSchema
)
}
case class ThingOneSpec(
name: String,
age: Long
) extends ThingSpec {
def kind: String = "one"
}
object ThingOneSpec {
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
implicit val thingConfigEncoder: Encoder[ThingOneSpec] = deriveEncoder(
derivation.renaming.snakeCase
)
implicit val thingConfigDecoder: Decoder[ThingOneSpec] = deriveDecoder(
derivation.renaming.snakeCase
)
implicit val thingConfigSchema: Schema[ThingOneSpec] = Schema.derived
}
case class ThingTwoSpec(
height: Long,
weight: Long,
) extends ThingSpec {
def kind: String = "two"
}
object ThingTwoSpec {
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
implicit val thingConfigEncoder: Encoder[ThingTwoSpec] = deriveEncoder(
derivation.renaming.snakeCase
)
implicit val thingConfigDecoder: Decoder[ThingTwoSpec] = deriveDecoder(
derivation.renaming.snakeCase
)
implicit val thingConfigSchema: Schema[ThingTwoSpec] = Schema.derived
}
Which seems to be working OK - except for the redoc docs which are generated. The "request body section" of the redoc, which I believe is generated from
.in(jsonBody[ThingSpec].description("Specification of the thing"))
only includes details of the ThingOneSpec object, there is no mention of ThingTwoSpec. The "payload" example section includes both.
My main question is how to get the request body section of the docs to show both possible payloads.
However - I'm aware that I might not have done this in the best way (from a circe/tapir point of view). Ideally, I'd like not to include an explicit discriminator (kind
) in the trait/classes, because I'd rather it not be exposed to the end user in the 'Payload' sections of the docs. Despite reading
- https://tapir.softwaremill.com/en/v0.17.7/endpoint/customtypes.html
- https://github.com/softwaremill/tapir/blob/master/examples/src/main/scala/sttp/tapir/examples/custom_types/SealedTraitWithDiscriminator.scala
- https://github.com/softwaremill/tapir/issues/315
I cannot get this working without the explicit discriminator.
答案1
得分: 1
你可以通过手动定义一个“one-of”模式来摆脱鉴别器:
implicit val thingConfigSchema: Schema[ThingSpec] =
Schema(
SchemaType.SCoproduct(List(ThingOneSpec.thingConfigSchema, ThingTwoSpec.thingConfigSchema), None) {
case one: ThingOneSpec => Some(SchemaWithValue(ThingOneSpec.thingConfigSchema, one))
case two: ThingTwoSpec => Some(SchemaWithValue(ThingTwoSpec.thingConfigSchema, two))
},
Some(Schema.SName(ThingSpec.getClass.getName))
)
(是的,写起来确实很困难;我会查看是否可以通过宏或其他方式生成。)
当由redoc渲染时,我会得到一个“one of”开关,所以我认为这是期望的结果:
英文:
You can get rid of the discriminator by defining a one-of schema by hand:
implicit val thingConfigSchema: Schema[ThingSpec] =
Schema(
SchemaType.SCoproduct(List(ThingOneSpec.thingConfigSchema, ThingTwoSpec.thingConfigSchema), None) {
case one: ThingOneSpec => Some(SchemaWithValue(ThingOneSpec.thingConfigSchema, one))
case two: ThingTwoSpec => Some(SchemaWithValue(ThingTwoSpec.thingConfigSchema, two))
},
Some(Schema.SName(ThingSpec.getClass.getName))
)
(Yes, it is unnecessarily hard to write; I'll look if this can be possibly generated by a macro or otherwise.)
When rendered by redoc, I get a "one of" switch, so I think this is the desired outcome:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论