Search code examples
scalatapirmagnolia-scala

Transform constructor names for Tapir Schemas


The JSON library 'Circe' has a Configuration param, transformConstructorNames: String => String, which allows you to transform the names of any class.

Is there an equivalent for Tapir?

The reason is that Tapir's Schema.derived[OpenAPISealedTrait] works great, but I want to drop the "OpenAPI" part from every class in the hierarchy.

For example, if I have

    sealed trait OpenAPIShape
    case class OpenAPISquare(size: Int) extends OpenAPIShape
    case class OpenAPICircle(radius: Int) extends OpenAPIShape

I want to be able to write Schema.derived[OpenAPIShape] with some config that drops "OpenAPI" from the title/name of each generated Schema. So that in the yaml the names are Square, Circle and Shape.

Is this possible? (Also is it possible without having to write a different Schema.derived for every point in the hierarchy?)

Update: In response to @adamw, thank you for the answer. I believe you mean I should do the following:


  implicit val sShape: Schema[OpenAPIShape] = {
    implicit val config: Configuration = Configuration.default
      .copy(
        toDiscriminatorValue = { name =>
          val replacement = name.fullName.split('.').last.stripSuffix("$").replaceAll("OpenAPI", "")
          replacement
        }
      )
    Schema.derived
  }

But this doesn't seem to work. The Swagger UI still refers to 'OpenAPIShape' etc. swagger ui

Am I doing something wrong?

A note, that Schema.derived is defined as def derived[T]: Schema[T] = macro Magnolia.gen[T]. So it does not take an implicit Configuration. It's possible some macro magic is happening that I'm missing.

Update 2:

Thanks @adamw. The following works

  import sttp.tapir._
  import sttp.tapir.generic.auto._
  import sttp.tapir.generic.Derived

  private implicit val sCircle: Schema[OpenAPICircle] =
    implicitly[Derived[Schema[OpenAPICircle]]].value.copy(name = Some(SName("Circle")))
  private implicit val sSquare: Schema[OpenAPISquare] =
    implicitly[Derived[Schema[OpenAPISquare]]].value.copy(name = Some(SName("Square")))
  private implicit val sShape: Schema[OpenAPIShape] =
    implicitly[Derived[Schema[OpenAPIShape]]].value.copy(name = Some(SName("Shape")))

So does

@encodedName("Shape")
sealed trait OpenAPIShape
@encodedName("Square")
case class OpenAPISquare(sizeInt: Int) extends OpenAPIShape
@encodedName("Circle")
case class OpenAPICircle(radiusInt: Int) extends OpenAPIShape

Solution

  • Class names can be used in different contexts in tapir.

    First, if you'd like to customise the labels that are visible in the OpenAPI schema (but are not themself part of the data model), this is possible by customising the schema.

    One possibility is to annotate the class using @encodedName. Another is to modify the derived schema, e.g.:

    implicitly[Derived[Schema[MyClass]]].value.copy(name = Some(...))
    

    More in the docs.

    Second, class names can be used as discriminator values for type hierarchies.

    For this purpose, you can configure tapir's schema derivation in a very similar way to circe's codec derivation, by providing an implicit sttp.tapir.generic.Configuration.

    There, you can modify the toDiscriminatorValue member function, which allows transforming the discriminator values. If class names are used as discriminators, this will be used for derivation.

    If you want to avoid having to write Schema.derived for every class, you can use auto-derivation by importing sttp.tapir.generic.auto._. However, this might have negative performance implications.

    See also the docs, which cover both topics.