Migration from the Json API
The Json API and the new validation API are really similar. One could see the new Validation API as just an evolution of the Json API.
The json validation API still works just fine but we recommend you use the new validation API for new code, and to port your old code whenever it's possible.
Reads
migration
The equivalent of a Json Reads
is a Rule
. The key difference is that Reads
assumes Json input, while Rule
is more generic, and therefore has one more type parameter.
Basically Reads[String]
== Rule[JsValue, String]
.
Migrating a Json Reads
to a Rule
is just a matter of modifying imports and specifying the input type.
Let's take a typical example from the Json API documentation:
case class Creature(
name: String,
isDead: Boolean,
weight: Float)
Using the json API, you would have defined something like:
scala> {
| import play.api.libs.json._
| import play.api.libs.functional.syntax._
|
| implicit val creatureReads = (
| (__ \ "name").read[String] and
| (__ \ "isDead").read[Boolean] and
| (__ \ "weight").read[Float]
| )(Creature.apply _)
|
| val js = Json.obj( "name" -> "gremlins", "isDead" -> false, "weight" -> 1.0F)
| Json.fromJson[Creature](js)
| }
res0: play.api.libs.json.JsResult[Creature] = JsSuccess(Creature(gremlins,false,1.0),)
Using the new API, this code becomes:
import jto.validation._
import play.api.libs.json._
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))
Which apart from the extra imports is very similar. Notice the From[JsValue]{...}
block, that's one of the nice features of the new validation API. Not only it avoids type repetition, but it also scopes the implicits.
Important: Note that we're importing
Rules._
inside theFrom[JsValue]{...}
block. It is recommended to always follow this pattern, as it nicely scopes the implicits, avoiding conflicts and accidental shadowing.
readNullable
The readNullable method does not exists anymore. Just use a Rule[JsValue, Option[T]]
instead. null
and non existing Path will be handled correctly and give you a None
:
val nullableStringRule: Rule[JsValue, Option[String]] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
(__ \ "foo").read[Option[String]]
}
val js1 = Json.obj("foo" -> "bar")
val js2 = Json.obj("foo" -> JsNull)
val js3 = Json.obj()
scala> nullableStringRule.validate(js1)
res4: jto.validation.VA[Option[String]] = Valid(Some(bar))
scala> nullableStringRule.validate(js2)
res5: jto.validation.VA[Option[String]] = Valid(None)
scala> nullableStringRule.validate(js3)
res6: jto.validation.VA[Option[String]] = Valid(None)
keepAnd
The general use for keepAnd
is to apply two validation on the same JsValue
, for example:
{
import play.api.libs.json._
import Reads._
import play.api.libs.functional.syntax._
(JsPath \ "key1").read[String](email keepAnd minLength[String](5))
}
You can achieve the same thing in the Validation API using Rules composition
From[JsValue] { __ =>
import jto.validation.playjson.Rules._
(__ \ "key1").read(email |+| minLength(5))
}
lazy reads
Reads are always lazy in the new validation API, therefore, you don't need to use any specific function, even for recursive types:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class User(id: Long, name: String, friend: Option[User] = None)
implicit lazy val UserReads: Reads[User] = (
(__ \ 'id).read[Long] and
(__ \ 'name).read[String] and
(__ \ 'friend).lazyReadNullable(UserReads)
)(User.apply _)
val js = Json.obj(
"id" -> 123L,
"name" -> "bob",
"friend" -> Json.obj("id" -> 124L, "name" -> "john", "friend" -> JsNull))
scala> Json.fromJson[User](js)
res12: play.api.libs.json.JsResult[User] = JsSuccess(User(123,bob,Some(User(124,john,None))),)
becomes:
case class User(id: Long, name: String, friend: Option[User] = None)
implicit lazy val userRule: Rule[JsValue, User] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
((__ \ "id").read[Long] ~
(__ \ "name").read[String] ~
(__ \ "friend").read(optionR(userRule)))(User.apply)
}
val js = Json.obj(
"id" -> 123L,
"name" -> "bob",
"friend" -> Json.obj("id" -> 124L, "name" -> "john", "friend" -> JsNull))
scala> From[JsValue, User](js)
res15: jto.validation.VA[User] = Valid(User(123,bob,Some(User(124,john,None))))
Numeric types
You should be aware that numeric type coercion is a bit stricter in the validation API.
For example:
scala> val js = Json.obj("n" -> 42.5f)
js: play.api.libs.json.JsObject = {"n":42.5}
scala> js.validate((__ \ "n").read[Int]) // JsSuccess(42, /n)
res16: play.api.libs.json.JsResult[Int] = JsError(List((/n,List(ValidationError(List(error.expected.int),WrappedArray())))))
whereas with the validation API, an Int
must really be an Int
:
import json.Rules._
val js = Json.obj("n" -> 42.5f)
(Path \ "n").read[JsValue, Int].validate(js)
json.apply
and path.as[T]
Those methods do not exist in the validation API. Even in the json API, it is generally recommended not to use them as they are "unsafe".
The preferred solution is to use path.read[T]
and to handle failure properly.
{
val js = Json.obj("foo" -> "bar")
(js \ "foo").as[String]
}
becomes
{
import jto.validation.playjson.Rules._
(Path \ "foo").read[JsValue, String]
}
pickBranch
JsPath
has a prickBranch
method, that creates a Reads
extracting a subtree in a Json object:
For example, given the following json object, we can extract a sub tree:
{
import play.api.libs.json._
val js = Json.obj(
"field1" -> "alpha",
"field2" -> 123L,
"field3" -> Json.obj(
"field31" -> "beta",
"field32"-> 345
))
val pick = (__ \ "field3").json.pickBranch
pick.reads(js) // Valid({"field3":{"field31":"beta","field32":345}})
}
In the validation API, you simply use read
to create a rule picking a branch:
import jto.validation._
import play.api.libs.json._
val js = Json.obj(
"field1" -> "alpha",
"field2" -> 123L,
"field3" -> Json.obj(
"field31" -> "beta",
"field32"-> 345
))
val pick: Rule[JsValue, JsValue] = From[JsValue] { __ =>
import jto.validation.playjson.Rules._
(__ \ "field3").read[JsValue]
}
scala> pick.validate(js)
res22: jto.validation.VA[play.api.libs.json.JsValue] = Valid({"field31":"beta","field32":345})
Writes
migration
Writes
are really easy to port. Just like Reads
, it's basically a matter of adding imports.
For example, you would have defined a Writes
for the Creature
case class this way:
import play.api.libs.json._
import scala.Function.unlift
case class Creature(
name: String,
isDead: Boolean,
weight: Float)
implicit val creatureWrite = (
(__ \ "name").write[String] and
(__ \ "isDead").write[Boolean] and
(__ \ "weight").write[Float]
)(unlift(Creature.unapply))
Json.toJson(Creature("gremlins", false, 1f))
With the validation API:
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> val c = To[Creature, JsObject](Creature("gremlins", false, 1f))
c: play.api.libs.json.JsObject = {"name":"gremlins","isDead":false,"weight":1}
Format
migration
The validation API does not have an equivalent for Format
. We find that generally Format
is not really convenient since validation and serialization are rarely symmetrical, and you quite often end up having multiple Reads
for a given type, making Format
rather unsettling.
Json Inception (macro)
Macros are also available for the validation API. See Validation Inception.