Embedding the custom body model validation as a given Codec?

Say I have a User model that is an input to my server:

  case class User(name: String, age: Int)
  object User:
    val validator: Validator.Custom[User] =
      Validator.Custom[User] { u =>
        if u.age < 0 then
          Invalid("Age must be greater than or equal to 0")
        else
          Valid
      }

And I want to validate json input that the age is greater than 0, I could do this when defining my endpoint:

  private val jsonBodyExample: SyncEndpoint = endpoint
    .post
    .in("hello")
    .in(jsonBody[User].validate(User.validator))
    .out(stringBody)
    .handleSuccess(user => s"Hello ${user.name} who is ${user.age} years old")

But we can’t rely on every endpoint remembering to explicitly declare this validator. The docs seem to indicate that you can do this by providing a Codec:

given Codec[String, User, JsonSomething?] = ???

Am I reading that correctly? If so, then how do I construct a Codec given I already have a validator defined?

If not, then maybe I just need to define an alternative to jsonBody[User]… something like

  def validatedJsonBody[T: Encoder: Decoder: Schema]: EndpointIO.Body[String, T] =
    jsonBody[User].validate(_.validator)

Feels a little icky, but maybe the right way?

Ah, I did discover you can inline validations like this!

Am still curious how you would define a custom validator for a class like this though.

You can add your validator to Schema[User]:

import sttp.tapir.generic.auto._
import sttp.tapir.Validator
import sttp.tapir.ValidationResult.Invalid
import sttp.tapir.ValidationResult.Valid
import sttp.tapir.Schema

case class User(name: String, age: Int)
  object User:
    val validator: Validator.Custom[User] =
      Validator.Custom[User] { u =>
        if u.age < 0 then
          Invalid("Age must be greater than or equal to 0")
        else
          Valid
      }
    given userSchema: Schema[User] = 
      Schema.derived[User].validate(validator)
1 Like