How to create an endpoint with Tapir in Scala with multiple Schemas

906 views Asked by At

I’m just heading an issue when I’m trying to create an endpoint with multiple bodies shape.

My model looks like this:

sealed trait FileExampleTrait {
  def kind: String
}

case class FileExampleOne(name: String, length: Int) extends FileExampleTrait {
  override def kind: String = “one”
}

case class FileExampleTwo(name: String) extends FileExampleTrait {
  override def kind: String = “two”
}

case class FileExampleResponse(message: String)

And I’m trying to create this endpoint:

val fileExample = baseEndpoint.post
    .in(“example”)
    .in(jsonBody[FileExampleTrait])
    .out(jsonBody[FileExampleResponse])
    .summary(“something”)
    .description(“something”)

The implementation of the endpoint looks like this:

private val fileExample = toAkkaRoute(jwtConsumer, errorHandler)(
    FileApi.fileExample, { (scope: RequestScope, input: (FileExampleTrait)) =>
      print(scope)
      input match {
        case FileExampleOne(name, _) => Future.successful(FileExampleResponse(name).asRight)
        case FileExampleTwo(name) => Future.successful(FileExampleResponse(name).asRight)
      }
    }
  )

This is just an example on what I’m trying to create. I added the schema derivation based on this:

  val sOne = Schema.derived[FileExampleOne]
  val sTwo = Schema.derived[FileExampleTwo]
  implicit val sExampleTrait: Schema[FileExampleTrait] =
    Schema.oneOfUsingField[FileExampleTrait, String](_.kind, _.toString)(“one” -> sOne, “two” -> sTwo)

I created a test for trying the endpoint based on Akka HTTP:

   test(“Example test”) {
    new Fixture() {
      val request = FileExampleOne(“name”, 1)
      Post(s”/api/v1/files/example”, jsonEntity(request)).withHeaders(requestHeaders) ~> wrappedRoute ~> check {
        response should be(successful)
        contentType shouldEqual ContentTypes.`application/json`
      }
    }
  }

The error I’m getting is the following:

Response error: {“code”:400,“message”:“Invalid value for: body (No constructor for type FileExampleTrait, JObject(List((name,JString(name)), (length,JInt(1)))))“}

I was following this documentation.

1

There are 1 answers

2
AminMal On

Well that's because a trait doesn't have a constructor as indicated in the error itself. I think I see where you're going, you want to try parsing the body as one of the traits subclasses. So imagine you have this type/class hierarchy:

        T // parent trait
       / \
     C1  C2  // class 1, etc...
    /
  C3

Now you want to deserialize some JSON into trait T, you need to define your custom behavior, like "First try converting into C3, if failed, try converting to C2, if failed again, try converting to C1", and you'll get your T value. Now depending on the JSON library you use, the implementation might differ, see the documentation by softwaremill to get more information about how to deal with JSONs in tapir, and if you use Play Json, I can recommend:

object FileExampleOne {
  implicit val reader: Reads[FileExampleOne] = Json.reads
}

object FileExampleTwo {
  implicit val reader: Reads[FileExampleTwo] = Json.reads
}

object FileExampleTrait {
  implicit val reads: Reads[FileExampleTrait] = json => {
    json.validate[FileExampleOne] orElse json.validate[FileExampleTwo]
  }
}

You can see it running on scastie. And based on the tapir documentations, you need a Codec for your types, and one of the approaches to create your coded for JSON, is using one of tapir's supported libraries (circe, Play Json, ...).