Reputation: 7101
We would like to base some of our JSON validations to the result of the validation of a previous validation.
case class InsideObject(name: Option[String], companyName: Option[String], eType: Int)
We have the case class above and we would like to introduce a JSON validation that will make the name
required if eType = 1
or make the companyName
required if eType = 2
plus some more specific validations per field that we already have in place while reading the JSON object.
implicit val insideObjectReads: Reads[InsideObject] = (
(JsPath \ "name").readNullable[String]
.filter(JsonValidationError("must be 10 digits"))(name => name.getOrElse("").length == 10) and
(JsPath \ "companyName").readNullable[String]
.filter(JsonValidationError("must not be optional"))(companyName => companyName.isDefined) and
(JsPath \ "eType").read[Int]
.filter(JsonValidationError("eType can only take values 1 or 2"))(eType => eType == 1 || eType == 2)
)(InsideObject.apply _)
We can do these validation inside the apply
method but we would like to do it while reading the JSON object. Any suggestions?
Sample code: https://scastie.scala-lang.org/wTuhI1zCSJSWKu9Lltcqbw
Upvotes: 2
Views: 222
Reputation: 3922
You could chain the Reads using orElse.
val reads1: Reads[InsideObject] = (
(JsPath \ "name").readNullable[String]
.filter(JsonValidationError("must not be optional"))(name => name.isDefined) and
(JsPath \ "companyName").readNullable[String] and
(JsPath \ "eType").read[Int]
.filter(JsonValidationError("eType can only take values 1 or 2"))(eType => eType == 1)
) (InsideObject.apply _)
implicit val reads2: Reads[InsideObject] = (
(JsPath \ "name").readNullable[String] and
(JsPath \ "companyName").readNullable[String]
.filter(JsonValidationError("must not be optional"))(companyName => companyName.isDefined) and
(JsPath \ "eType").read[Int]
.filter(JsonValidationError("eType can only take values 1 or 2"))(eType => eType == 2)
) (InsideObject.apply _).orElse(reads1)
Here, in the first Reads
, companyName is required with eType as 2. In case that results in JsError
, the second runs where name is required and eType is 1
--Edit--
Assuming your case class has extra fields like below:
case class InsideObject(age: Option[Int], location: Option[String], name: Option[String], companyName: Option[String], eType: Int)
object InsideObject {
implicit val writesPublicLeadFormRequest: OFormat[InsideObject] = Json.format[InsideObject]
def apply(age: Option[Int], location: Option[String],name: Option[String], companyName: Option[String], eType: Int) =
new InsideObject(age, location, name, companyName, eType)
}
You can combine the Reads
val reads0 = (JsPath \ "age").readNullable[Int] and
(JsPath \ "location").readNullable[String]
val reads1: Reads[InsideObject] = (
reads0 and
(JsPath \ "name").readNullable[String]
.filter(JsonValidationError("must not be optional"))(name => name.isDefined) and
(JsPath \ "companyName").readNullable[String] and
(JsPath \ "eType").read[Int]
.filter(JsonValidationError("eType can only take values 1 or 2"))(eType => eType == 1)
) (InsideObject.apply _)
implicit val reads2: Reads[InsideObject] = (
reads0 and
(JsPath \ "name").readNullable[String] and
(JsPath \ "companyName").readNullable[String]
.filter(JsonValidationError("must not be optional"))(companyName => companyName.isDefined) and
(JsPath \ "eType").read[Int]
.filter(JsonValidationError("eType can only take values 1 or 2"))(eType => eType == 2)
) (InsideObject.apply _).orElse(reads1)
Hopefully, this will address the comment.
--Edit-2--
You can also pattern match on the json
, which I guess would give better flexibility in this particular case. Sample code:
implicit val reads3: Reads[InsideObject] = {
case JsObject(map @ Seq(("age", JsNumber(age)), ("location", JsString(location)), ("name", JsString(name)), ("companyName", JsString(companyName)), ("eType", JsNumber(eType)))) =>
if (eType == 1 && name.nonEmpty)
JsSuccess(InsideObject(Some(age.intValue), Some(location), Some(name), Some(companyName), 1))
else if (eType == 2 && companyName.nonEmpty)
JsSuccess(InsideObject(Some(age.intValue), Some(location), Some(name), Some(companyName), 2))
else
JsError("Custom Error - wrong eType?")
case _ => JsError("Custom Error - wrong number of fields?")
}
Instead of pattern matching on Seq
you could also use the underlying Map
for further validation. When using only Seq
, double-check on the number of fields, ie, a missing field will lead to JsError
in the above snippet.
--Edit-3--
On further inspection, I noticed that Edit-2
doesn't work as expected. I was testing using an older play-json and scala versions. In the scastie snippet, I noticed you were using 2.10.0-RC5
. The below solution was tested with 2.10.0-RC5
.
implicit val reads4: Reads[InsideObject] = {
case JsObject(map) =>
val opt: Option[(Option[String], Option[String], Int)] = for {
eType <- map.get("eType").map(_.as[Int])
name = map.get("name").flatMap(_.asOpt[String])
company = map.get("companyName").flatMap(_.asOpt[String])
} yield (name, company, eType)
opt match {
case Some((name, company, eType)) =>
if (eType == 1 && name.isDefined)
JsSuccess(InsideObject(name, company, 1))
else if (eType == 2 && company.isDefined)
JsSuccess(InsideObject(name, company, 2))
else
JsError("Custom Error - wrong eType?")
case None => JsError("Custom Error - wrong number of fields")
}
case _ => JsError("Custom Error - wrong type?")
}
The updated version can be seen here.
Upvotes: 1