What would be an idiomatic way to define mutually exclusive inputs?

e.g (in my case): There’s different query parameters, which exactly one of them is required and should be supplied. not both.

I can take 2 options: (Option[String], Option[String]) and validate as part of the logic, but ideally I would want to decode the request into an Either[String, String] input. And if possible, add a documented validation for it.

Thanks!

I don’t think the OpenAPI spec allows multi-parameter validators? (please correct me if I’m wrong). So you won’t be able to document this other than in words.

That said, in tapir you can do a .mapDecode on the combined input:

query[Option[String]]("a").and(query[Option[String]]("b")).mapDecode { 
  case (Some(a), Some(b)) => DecodeResult.Multiple(List(a, b)) // will just return a 400 Bad Request
  case (None, None) => DecodeResult.Missing 
  case (Some(a), None) => Left(a)
  case (None, Some(b)) => Right(b)
} { 
  case Left(a) => (Some(a), None)
  case Right(b) => (None, Some(b))
}

To provide better error messages returned to the user, I think it would be easiest to extend the DefaultDecodeFailureHandler to handle the Multiple/Missing decode failures for the combined inputs. You can mark these special inputs with an attribute, and then check in your DecodeFailureHandler if the endpoint input has this attribute.

Alternatively, you can return DecodeResult.InvalidValue with a list of validation errors, optionally providing custom messages, which will be included in the error message. This option doesn’t require you to modify the DDFH.

Awesome! Thanks @adamw , I ended up using DecodeResult.InvalidValue and it works great.

I do wish for a more “ergonomic” experience, not sure if it’ll make sense, or possible at all, but here goes:
Currently, I have something of this sort:

def decodeLogic(left: Option[String], right: Option[String]): DecodeResult[Either[String, String]] = {
  val bothSuppliedMsg = "Both left & right parameters were supplied, but exactly 1 is needed"
  (left, right) match {
    case (Some(r), Some(l)) => DecodeResult.InvalidValue(
      List(
        ValidationError(
          Validator.Custom[Either[String, String]](_ => ValidationResult.Invalid(bothSuppliedMsg), Some(bothSuppliedMsg)),
          List(s"left=$l", s"right=$r"),
          List(FieldName("left"), FieldName("right")),
          Some(bothSuppliedMsg)
        )
      )
    )
    case ... => ...
  }
}

Which:

  • is a bit verbose
  • isn’t flexible

I really like the fact the I’m able to define the endpoint with input of Either[String, String].
By doing so, I can guarantee that whoever uses the API with the derived sttp client cannot go wrong.

However, I can’t control the output format. It’s just a string (text/plain). It would be super nice to be able to supply an EndpointOutput or even just an EndpointIO.Body instead of a plain string, and of course have it also documented as part of the schema.

Also, a bit less boilerplate would be less intimidating for newbies and easier for me to advocate for more tapir in our code :grin: