Cookbook
All the examples below are validating Json objects. The API is not dedicated only to Json, it can be used on any type. Please refer to Validating Json, Validating Forms, and Supporting new types for more information.
Rule
Typical case class validation
import jto.validation._
import play.api.libs.json._
case class Creature(
name: String,
isDead: Boolean,
weight: Float)
implicit val creatureRule: Rule[JsValue, Creature] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
((__ \ "name").read[String] ~
(__ \ "isDead").read[Boolean] ~
(__ \ "weight").read[Float])(Creature.apply)
}
scala> val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0f)
js: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
scala> From[JsValue, Creature](js)
res2: jto.validation.VA[Creature] = Valid(Creature(gremlins,false,1.0))
scala> From[JsValue, Creature](Json.obj())
res3: jto.validation.VA[Creature] = Invalid(List((/name,List(ValidationError(List(error.required),WrappedArray()))), (/isDead,List(ValidationError(List(error.required),WrappedArray()))), (/weight,List(ValidationError(List(error.required),WrappedArray())))))
Dependent values
A common example of this use case is the validation of password
and password confirmation
fields in a signup form.
- First, you need to validate that each field is valid independently
- Then, given the two values, you need to validate that they are equals.
import jto.validation._
import play.api.libs.json._
val passRule: Rule[JsValue, String] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
// This code creates a `Rule[JsValue, (String, String)]` each of of the String must be non-empty
((__ \ "password").read(notEmpty) ~
(__ \ "verify").read(notEmpty)).tupled
// We then create a `Rule[(String, String), String]` validating that given a `(String, String)`,
// both strings are equals. Those rules are then composed together.
.andThen(Rule.uncurry(equalTo[String])
// In case of `Invalid`, we want to control the field holding the errors.
// We change the `Path` of errors using `repath`
.repath(_ => (Path \ "verify"))
)
}
Let's test it:
scala> passRule.validate(Json.obj("password" -> "foo", "verify" -> "foo"))
res5: jto.validation.VA[String] = Valid(foo)
scala> passRule.validate(Json.obj("password" -> "", "verify" -> "foo"))
res6: jto.validation.VA[String] = Invalid(List((/password,List(ValidationError(List(error.required),WrappedArray())))))
scala> passRule.validate(Json.obj("password" -> "foo", "verify" -> ""))
res7: jto.validation.VA[String] = Invalid(List((/verify,List(ValidationError(List(error.required),WrappedArray())))))
scala> passRule.validate(Json.obj("password" -> "", "verify" -> ""))
res8: jto.validation.VA[String] = Invalid(List((/password,List(ValidationError(List(error.required),WrappedArray()))), (/verify,List(ValidationError(List(error.required),WrappedArray())))))
scala> passRule.validate(Json.obj("password" -> "foo", "verify" -> "bar"))
res9: jto.validation.VA[String] = Invalid(List((/verify,List(ValidationError(List(error.equals),WrappedArray(foo))))))
Recursive types
When validating recursive types:
- Use the
lazy
keyword to allow forward reference. - As with any recursive definition, the type of the
Rule
must be explicitly given.
case class User(
name: String,
age: Int,
email: Option[String],
isAlive: Boolean,
friend: Option[User])
import jto.validation._
import play.api.libs.json._
// Note the lazy keyword
implicit lazy val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
((__ \ "name").read[String] ~
(__ \ "age").read[Int] ~
(__ \ "email").read[Option[String]] ~
(__ \ "isAlive").read[Boolean] ~
(__ \ "friend").read[Option[User]])(User.apply)
}
or using macros:
scala> import jto.validation._
import jto.validation._
scala> import play.api.libs.json._
import play.api.libs.json._
scala> import jto.validation.playjson.Rules._
import jto.validation.playjson.Rules._
scala> // Note the lazy keyword, and the explicit typing
| implicit lazy val userRule: Rule[JsValue, User] = Rule.gen[JsValue, User]
userRule: jto.validation.Rule[play.api.libs.json.JsValue,User] = <lazy>
Read keys
import jto.validation._
import play.api.libs.json._
val js = Json.parse("""
{
"values": [
{ "foo": "bar" },
{ "bar": "baz" }
]
}
""")
val r: Rule[JsValue, Seq[(String, String)]] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
val tupleR: Rule[JsValue, (String, String)] = Rule.fromMapping[JsValue, (String, String)] {
case JsObject(Seq((key, JsString(value)))) => Valid(key.toString -> value)
case _ => Invalid(Seq(ValidationError("BAAAM")))
}
(__ \ "values").read(seqR(tupleR))
}
scala> r.validate(js)
res15: jto.validation.VA[Seq[(String, String)]] = Invalid(List((/values[0],List(ValidationError(List(BAAAM),WrappedArray()))), (/values[1],List(ValidationError(List(BAAAM),WrappedArray())))))
Validate subclasses (and parse the concrete class)
Consider the following class definitions:
trait A
case class B(foo: Int) extends A
case class C(bar: Int) extends A
val b = Json.obj("name" -> "B", "foo" -> 4)
val c = Json.obj("name" -> "C", "bar" -> 6)
val e = Json.obj("name" -> "E", "eee" -> 6)
Trying all the possible rules implementations
import cats.syntax.cartesian._
val rb: Rule[JsValue, A] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
(__ \ "name").read(equalTo("B")) *> (__ \ "foo").read[Int].map(B.apply)
}
val rc: Rule[JsValue, A] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
(__ \ "name").read(equalTo("C")) *> (__ \ "bar").read[Int].map(C.apply)
}
val typeInvalid = Invalid(Seq(Path -> Seq(ValidationError("validation.unknownType"))))
val rule = rb orElse rc orElse Rule(_ => typeInvalid)
scala> rule.validate(b)
res20: jto.validation.VA[A] = Valid(B(4))
scala> rule.validate(c)
res21: jto.validation.VA[A] = Valid(C(6))
scala> rule.validate(e)
res22: jto.validation.VA[A] = Invalid(List((/,List(ValidationError(List(validation.unknownType),WrappedArray())))))
Using class discovery based on field discrimination
val typeInvalid = Invalid(Seq(Path -> Seq(ValidationError("validation.unknownType"))))
val rule: Rule[JsValue, A] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
(__ \ "name").read[String].flatMap[A] {
case "B" => (__ \ "foo").read[Int].map(B.apply _)
case "C" => (__ \ "bar").read[Int].map(C.apply _)
case _ => Rule(_ => typeInvalid)
}
}
scala> rule.validate(b)
res24: jto.validation.VA[A] = Valid(B(4))
scala> rule.validate(c)
res25: jto.validation.VA[A] = Valid(C(6))
scala> rule.validate(e)
res26: jto.validation.VA[A] = Invalid(List((/,List(ValidationError(List(validation.unknownType),WrappedArray())))))
Write
typical case class Write
import jto.validation._
import play.api.libs.json._
import scala.Function.unlift
case class Creature(
name: String,
isDead: Boolean,
weight: Float)
implicit val creatureWrite = To[JsObject] { __ =>
import jto.validation.playjson.Writes._
((__ \ "name").write[String] ~
(__ \ "isDead").write[Boolean] ~
(__ \ "weight").write[Float])(unlift(Creature.unapply))
}
scala> To[Creature, JsObject](Creature("gremlins", false, 1f))
res29: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
Adding static values to a Write
import jto.validation._
import play.api.libs.json._
case class LatLong(lat: Float, long: Float)
implicit val latLongWrite = {
import jto.validation.playjson.Writes._
To[JsObject] { __ =>
((__ \ "lat").write[Float] ~
(__ \ "long").write[Float])(unlift(LatLong.unapply))
}
}
case class Point(coords: LatLong)
implicit val pointWrite = {
import jto.validation.playjson.Writes._
To[JsObject] { __ =>
((__ \ "coords").write[LatLong] ~
(__ \ "type").write[String]) ((_: Point).coords -> "point")
}
}
scala> val p = Point(LatLong(123.3F, 334.5F))
p: Point = Point(LatLong(123.3,334.5))
scala> pointWrite.writes(p)
res34: play.api.libs.json.JsObject = {"coords":{"lat":123.3,"long":334.5},"type":"point"}