Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validate json property with dependency of other property in Play 2.4

I have

case class AclRuleScope(kind: String, value: String)

and i want convert Json to AclRuleScope with restrictions:

type may be only: "default" | "user" | "group" | "domain"

value may be only email if type is "user" | "group", and some string in another cases

I have object with Reader and Writer, but i cant understand how can i get type value when reading value:

object AclRuleScope {

  implicit val aclRuleScopeRead = (
    (__ \ "type").read[String](pattern("^(default|user|group|domain)$".r)) and
      (__ \ "value").read[String](
          email keepAnd 
          filter(
              ValidationError("error.scope.value")
          )( ??? == JsString("user") || ??? == JsString("group")))
    )(this.apply _)

}

What must be in ???

like image 233
andrey.ladniy Avatar asked Oct 22 '25 01:10

andrey.ladniy


2 Answers

JsConstraints#filter has the following signature

def filter[A](otherwise: ValidationError)(p: A => Boolean)(implicit reads: Reads[A])

when you write

filter(ValidationError("error.scope.value"))(??? == JsString("user") || ??? == JsString("group")))

the code (??? == JsString("user") || ??? == JsString("group")) is actually the second parameter to filter, it should therefore be a predicate of A => Boolean. Also since this is applied after the email reads which is a Reads[String], your actual A is String so you should drop the JsString.

The smallest change you could write is:

implicit val aclRuleScopeRead = (
  (__ \ "type").read[String](pattern("^(default|user|group|domain)$".r)) and
    (__ \ "value").read[String](
      email keepAnd
        filter(
          ValidationError("error.scope.value")
        )(x => x == "user" || x == "group"))
  ).tupled

I strongly encourage you to extract the predicate to its own method:

def isValidEmail: (String) => Boolean = {
  x => x == "user" || x == "group"
}

and write your reads as

implicit val aclRuleScopeRead = (
  (__ \ "type").read[String](pattern("^(default|user|group|domain)$".r)) and
    (__ \ "value").read[String](
      email keepAnd filter(ValidationError("error.scope.value"))(isValidEmail))
  ).tupled

even better you could have

val validEmail = email keepAnd filter(ValidationError("error.scope.value"))(isValidEmail))

and write

implicit val aclRuleScopeRead = (
  (__ \ "type").read[String](pattern("^(default|user|group|domain)$".r)) and
    (__ \ "value").read[String](validEmail)
  ).tupled

Upon clarification in the comments, you only want to parse the email if the type is "user" or "group" returning an empty string if not the case.

The answer is close to the solution outlined in this question

The reads of the value field first needs to check the value of the type field. The condition on the type field looks like :

(__ \ "type").read[String].filter(ValidationError("error.scope.value"))(isEmailType)

where isEmailType is defined as

def isEmailType: (String) => Boolean = { x => x == "user" || x == "group" }

This will return a read that gives a JsSuccess if the type is user or group and a JsError otherwise. From the comments we know that we should return the empty string if the type is not user or group, the reads can become:

(__ \ "type").read[String]
            .filter(ValidationError("error.scope.value"))(isEmailType)
            .orElse Reads.pure("")

Which is safe and will never return a JsError. This is fine since there is a dedicated reads to enforce the validation on type, the reads we are currently manipulating is only there as part of the validation of value. Now we need to change the read to parse value if the parsing is a JsSuccess :

(__ \ "type").read[String]
            .filter(ValidationError("error.scope.value"))(isEmailType)
            .flatMap(_ => (__ \ "value").read[String](email))
            .orElse Reads.pure("")

Using flatMap, we replace the type reads by the correct reads on value if the type reads is a success.

like image 170
Jean Avatar answered Oct 23 '25 15:10

Jean


Thanks @Jean, you are pointing me in the right direction.

flatMap and filter helps me, and i have

case class AclRuleScope(kind: String, value: Option[String])

val aclRuleScopeRead = (
    (__ \ "type").read[String] and
    (__ \ "type").read[String](pattern("^(default|user|group|domain)$".r)).flatMap {
      case t if "user".equals(t) || "group".equals(t) =>
        (__ \ "value").readNullable[String](email)
          .filter(ValidationError(s"error.acl.scope.value omitted for '$t' type"))(_.isDefined)
          .filter(ValidationError(s"error.acl.scope.value not defained for '$t' type"))(_.exists(_.nonEmpty))
      case "domain" =>
        (__ \ "value").readNullable[String]
          .filter(ValidationError("error.acl.scope.value omitted for 'domain' type"))(_.isDefined)
          .filter(ValidationError("error.acl.scope.value not defained for 'domain' type"))(_.exists(_.nonEmpty))
      case "default" =>
        (__ \ "value").readNullable[String]
          .filter(ValidationError("error.acl.scope.value must be omitted for 'default' type"))(_.isEmpty)
    }
)(AclRuleScope.apply _)
like image 43
andrey.ladniy Avatar answered Oct 23 '25 16:10

andrey.ladniy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!