JSON play: reading and checking JsObject with unknown keys

I am reading a nested JSON document using multiple implementations Reads[T], however I am stuck in the following sub-object:

{
    ...,
    "attributes": {
        "keyA": [1.68, 5.47, 3.57],
        "KeyB": [true],
        "keyC": ["Lorem", "Ipsum"]
     },
     ...
}

The keys ("keyA", "keyB" ...), as well as the number of keys are not known at compile time and may vary. Key values ​​are always JsArrayinstances, but of different sizes and types (however, all elements of a particular array must have the same type JsValue).

Scala representation of a single attribute:

case class Attribute[A](name: String, values: Seq[A])
// 'A' can only be String, Boolean or Double

The goal is to create Reads[Seq[Attribute]] which can be used for the "attributes" field when transforming the entire document (remember, "attributes" is just a sub-document).

, , . : (, , json-). , .

val required = Map(
  "KeyA" -> "Double",
  "KeyB" -> "String",
  "KeyD" -> "String",
)

, JSON, , Reads :

  • "keyB" , ( , ).
  • "keyD" ( keyC ).

Reads. , , Reads:

...
(__ \ "attributes").reads[Map[String, JsArray]]...
...

, , JSON , String JsArray -, Reads . , : , . , , Map Seq[Attribute], - JsResult, .

, :

  val attributeSeqReads = new Reads[Seq[Attribute]] {
    def reads(json: JsValue) = json match {
      case JsObject(fields) => processAttributes(fields)
      case _ => JsError("attributes not an object")
    }
    def processAttributes(fields: Map[String, JsValue]): JsResult[Seq[Attribute]] = {
      // ...
    }
  }

, processAttributes. , . .

:

, (keyA, keyB...) . , required, . , , required / . , , required.

+4
1

( "keyA", "keyB"...),

, ?

, JSON, , Reads :

  • "keyB" , ( , ).

  • "keyD" ( keyC ).

- ?

Reads[Attribute] Reads.list(Reads.of[A]) ( ) ( ) Reads.pure(Attribute[A]). (_.productIterator.toList), Seq[Attribute]

val r = (
  (__ \ "attributes" \ "keyA").read[Attribute[Double]](list(of[Double]).map(Attribute("keyA", _))) and
    (__ \ "attributes" \ "keyB").read[Attribute[Boolean]](list(of[Boolean]).map(Attribute("keyB", _))) and
    ((__ \ "attributes" \ "keyC").read[Attribute[String]](list(of[String]).map(Attribute("keyC", _))) or Reads.pure(Attribute[String]("keyC", List()))) and 
    (__ \ "attributes" \ "keyD").read[Attribute[String]](list(of[String]).map(Attribute("keyD", _)))        
  ).tupled.map(_.productIterator.toList)

scala>json1: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyD":["Lorem","Ipsum"]}}

scala>res37: play.api.libs.json.JsResult[List[Any]] = JsSuccess(List(Attribute(keyA,List(1.68, 5.47, 3.57)), Attribute(KeyB,List(true)), Attribute(keyC,List()), Attribute(KeyD,List(Lorem, Ipsum))),)   

scala>json2: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyC":["Lorem","Ipsum"]}}    

scala>res38: play.api.libs.json.JsResult[List[Any]] = JsError(List((/attributes/keyD,List(ValidationError(List(error.path.missing),WrappedArray())))))    

scala>json3: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":["Lorem"],"keyC":["Lorem","Ipsum"]}}    

scala>res42: play.api.libs.json.JsResult[List[Any]] = JsError(List((/attributes/keyD,List(ValidationError(List(error.path.missing),WrappedArray()))), (/attributes/keyB(0),List(ValidationError(List(error.expected.jsboolean),WrappedArray())))))

22 , : 22 .

'Reads.traversableReads [F [_], A]'

def attributesReads(required: Map[String, String]) = Reads {json =>
  type Errors = Seq[(JsPath, Seq[ValidationError])]

  def locate(e: Errors, idx: Int) = e.map { case (p, valerr) => (JsPath(idx)) ++ p -> valerr }

  required.map{
    case (key, "Double") => (__ \  key).read[Attribute[Double]](list(of[Double]).map(Attribute(key, _))).reads(json)
    case (key, "String") => (__ \ key).read[Attribute[String]](list(of[String]).map(Attribute(key, _))).reads(json)
    case (key, "Boolean") => (__ \ key).read[Attribute[Boolean]](list(of[Boolean]).map(Attribute(key, _))).reads(json)
    case _ => JsError("")
  }.iterator.zipWithIndex.foldLeft(Right(Vector.empty): Either[Errors, Vector[Attribute[_ >: Double with String with Boolean]]]) {
      case (Right(vs), (JsSuccess(v, _), _)) => Right(vs :+ v)
      case (Right(_), (JsError(e), idx)) => Left(locate(e, idx))
      case (Left(e), (_: JsSuccess[_], _)) => Left(e)
      case (Left(e1), (JsError(e2), idx)) => Left(e1 ++ locate(e2, idx))
    }
  .fold(JsError.apply, { res =>
    JsSuccess(res.toList)
  })
}

(__ \ "attributes").read(attributesReads(Map("keyA" -> "Double"))).reads(json)

scala> json: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyD":["Lorem","Ipsum"]}}

scala> res0: play.api.libs.json.JsResult[List[Attribute[_ >: Double with String with Boolean]]] = JsSuccess(List(Attribute(keyA,List(1.68, 5.47, 3.57))),/attributes)
+1

All Articles