tapir icon indicating copy to clipboard operation
tapir copied to clipboard

Schema for `Nothing`

Open nartamonov opened this issue 2 years ago • 3 comments

I have the following generic model of responses from a server API:

  sealed trait Status[+E, +A]
  final case class Error[E](error: E) extends Status[E, Nothing]
  final case class Fatal(reason: String) extends Status[Nothing, Nothing]
  final case class Success[A](a: A) extends Status[Nothing, A]
  final case class Timeout() extends Status[Nothing, Nothing]

  implicit def statusSchema[A, E](implicit es: Schema[E], as: Schema[A]): Schema[Status[E, A]] =
    Schema.derived

It works fine if an API method returns something like Status[String, Unit]. But what if some method demoMethod is known not to respond with Error case? It could be encoded as Status[Nothing, Unit]:

  val demoMethod: PublicEndpoint[Args, Unit, Status[Nothing, Unit], Any] =
    basepoint
      .post
      .in("demo-method")
      .in(jsonBody[Args])
      .out(jsonBody[Status[Nothing, Unit]])

For this to work we need a shema for Nothing, in other words the absence of a value.

Currently I use Schema.any as a workaround:

  implicit val nothingSchema: Schema[Nothing] =
    Schema.any

But of course any has not the same meaning as we want.

nartamonov avatar Mar 22 '23 14:03 nartamonov

I think Schema.any is as good a choice as anything else. Question is, how should it end up being represented in OpenAPI?

adamw avatar Mar 22 '23 15:03 adamw

I see two possible approaches:

  1. Hard, but preferrable. Extending deriving algorithm to exclude from derived schema all products with one or more parameters of type Nothing. For example, Schema.derived[Status[String,Unit]] derives something like (pseudo-code for brievety):

    Schema(SCoproduct(
      List(
        Schema(SProduct("Error", /* ... */)),
        Schema(SProduct("Fatal", /* ... */)),
        Schema(SProduct("Success", /* ... */)),
        Schema(SProduct("Timeout", /* ... */))
    ))
    

    But Schema.derived[Status[Nothing,Unit]] could detect that the parameter error of subtype Error has the type Nothing so that it should be excluded:

    Schema(SCoproduct(
      List(
        Schema(SProduct("Fatal", /* ... */)),
        Schema(SProduct("Success", /* ... */)),
        Schema(SProduct("Timeout", /* ... */))
    ))
    

    Not sure is that possible with Scala 2/3 macros.

  2. Easy, but less ergonomic for end-users. Since "JSON Schema vocabularies are free to define their own extended type system", we can explicitly add the type nothing. Yes, it requires some training of readers of generated OpenAPI spec, but it clearly reveals intention behind a shema design.

nartamonov avatar Mar 23 '23 10:03 nartamonov

As for 2., I'm not sure if that would be of any use, as OpenAPI is valuable only as long as the readers know what this means.

  1. is more promising, though. Probably not fully-automatic (as there might be cases when the Nothing type parameter appears in a class that might be instantiated), and outside of a macro, but wouldn't it be possible to write a function Schema => Schema, which looks for fields where the type is Nothing, and filters out their containing objects? Something like a eliminateUninstantiable

adamw avatar Mar 27 '23 11:03 adamw