Validating data

For validating data you need to use the Validator typeclass.
A small example:

import ba.sake.validson.Validator

case class ValidatedData(num: Int, str: String, seq: Seq[String])

object ValidatedData:
  given Validator[ValidatedData] = Validator
    .derived[ValidatedData]
    .positive(_.num)
    .notBlank(_.str)
    .notEmptySeq(_.seq)

The ValidatedData can be any case class: json data, form data, query params..


Create a file validation.sc and paste this code into it:

    //> using scala "3.4.0"
//> using dep ba.sake::sharaf:0.4.0

import io.undertow.Undertow
import ba.sake.querson.QueryStringRW
import ba.sake.tupson.JsonRW
import ba.sake.validson.Validator
import ba.sake.sharaf.*, routing.*

case class Car(brand: String, model: String, quantity: Int) derives JsonRW
object Car:
  given Validator[Car] = Validator
    .derived[Car]
    .notBlank(_.brand)
    .notBlank(_.model)
    .nonnegative(_.quantity)

case class CarQuery(brand: String) derives QueryStringRW
object CarQuery:
  given Validator[CarQuery] = Validator
    .derived[CarQuery]
    .notBlank(_.brand)

case class CarApiResult(message: String) derives JsonRW

val routes = Routes:
  case GET() -> Path("cars") =>
    val qp = Request.current.queryParamsValidated[CarQuery]
    Response.withBody(CarApiResult(s"Query OK: ${qp}"))

  case POST() -> Path("cars") =>
    val json = Request.current.bodyJsonValidated[Car]
    Response.withBody(CarApiResult(s"JSON body OK: ${json}"))

Undertow.builder
  .addHttpListener(8181, "localhost")
  .setHandler(
    SharafHandler(routes).withExceptionMapper(ExceptionMapper.json)
  )
  .build
  .start()

println(s"Server started at http://localhost:8181")

Then run it like this:

scala-cli validation.sc 

Notice above that we used queryParamsValidated and not plain queryParams (does not validate query params).
Also, for JSON body parsing+validation we use bodyJsonValidated and not plain bodyJson (does not validate JSON body).


When you do a GET http://localhost:8181/cars?brand=
you will get a nice JSON error message with HTTP Status of 400 Bad Request:

{
  "instance": null,
  "invalidArguments": [
    {
      "reason": "must not be blank",
      "path": "$.brand",
      "value": ""
    }
  ],
  "detail": "",
  "type": null,
  "title": "Validation errors",
  "status": 400
}

The error message format follows the RFC 7807 problem detail.


When you do a POST http://localhost:8181/cars with a malformed body:

{
  "brand": " ",
  "model": "ML350",
  "quantity": -5
}

you will get these errors:

{
  "instance": null,
  "invalidArguments": [
    {
      "reason": "must not be blank",
      "path": "$.brand",
      "value": " "
    },
    {
      "reason": "must not be negative",
      "path": "$.quantity",
      "value": "-5"
    }
  ],
  "detail": "",
  "type": null,
  "title": "Validation errors",
  "status": 400
}