byomjan
byomjan

Reputation: 119

Parse json and based on value return a list of keys

Scala Experts , need your help . task is to parse this json and return list of ModelIds for store 'livingstone' which has 'organic' = true.

In this case only RT001 has organic value as true.Please help.

Note: trying to use existing liftweb library .

package Exercises


import net.liftweb.json.{DefaultFormats, _}


object test1 extends App {

  val json_response =
    """{
  "requestId": "91ee60d5f1b45e#316",
  "error": null,
  "errorMessages": [
  ],
  "entries": [

  {

    "modelId":"RT001",
    "sku": "SKU-ASC001",
    "store": "livingstone",
    "ttlInSeconds": 8000,
    "metadata": {
      "manufactured_date": "2019-01-22T01:25Z",
      "organic": "true"
    }
  },
  {

    "modelId":"RT002",
    "sku": "SKU-ASC002",
    "store": "livingstone",
    "ttlInSeconds": 8000,
    "metadata": {
      "manufactured_date": "2019-10-03T01:25Z",
      "organic": "false"
    }
  }

  ] }"""


  val json = parse(json_response)

  implicit val formats = DefaultFormats

  val elements = (json \\ "entries").children


  for (subs <- elements) {
    val m = subs.extract[Subs]
    println(s"Subscriptions: ${m.modelId}, ${m.store}")
    println(" k,v: " + m.metadata.exists(_ == ("organic", "true")))
  }

  case class Subs(modelId: String, store: String, metadata: Map[String, String])


}

Getting Error. Also need help how to filter based on store=living stone and organic=true .

Exception in thread "main" net.liftweb.json.MappingException: No usable value for modelId
Do not know how to convert JArray(List(JString(RT001), JString(RT002))) into class java.lang.String

Final Working code with help from experts:

  val json = parse(json_response)

  implicit val formats = DefaultFormats


  case class Sales(modelId: String, sku: String, store: String, ttlInSeconds: Int, metadata: Map[String, String])

  case class Response(entries: List[Sales])

  val json1 = parse(json_response)
  val response = json.extract[Response]

  val subs = response.entries.filter { e =>
    e.store == "livingstone" &&
      e.metadata.get("organic").contains("true")

  }

  subs.foreach(x=>println(x.modelId))

Upvotes: 0

Views: 1189

Answers (2)

Tim
Tim

Reputation: 27356

When processing JSON it is best to convert the whole structure to Scala and then process the Scala, rather than directly processing the JSON.

So create a Response class and extract that in a single operation, and then process the entries field as appropriate.

Here is some completely untested code:

case class Response(entries: List[Subs])

val json = parse(json_response)
val response = json.extract[Response]

val subs = response.entries.filter{e =>
    e.store == "livingstone" &&
    e.metadata.get("organic").contains("true")
  }

Note that this should work in any JSON library that allows you to extract a class from JSON.

Upvotes: 2

Ivan Kurchenko
Ivan Kurchenko

Reputation: 4063

So, as it was suggested in comments section already, you can proceed with circe library instead of Lift framework, because it is much more modern and widely used solution.

What you need to do - declare structures, like case class'es, which represents your json. It's not recommended approach, to operate over raw JSON - rule of thumb - parse it into some meaningful structure and then work with it.

Along with, structure declaration, you also Encoder and Decoder for let's say "non standard" cases - like Boolean which is String in cases of organic field.

In your case the code might look like:

object App {
  def main(args: Array[String]): Unit = {
    import io.circe._, io.circe.generic.semiauto._, io.circe.generic.auto._, io.circe.parser._

    /**
     * General purpose response wrapper. Entries type might differ, as I can suppose, that's why it is generic.
     * 'errorMessages' - since it is empty array in example, I can only guess about exact type. For sake of example
     * let's say it is strings. And same for 'error' field.
     */
    case class Response[E](requestId: String, error: Option[String], errorMessages: List[String], entries: List[E])
    object Response {
      implicit def decoder[E](implicit d: Decoder[E]): Decoder[Response[E]] = deriveDecoder[Response[E]]
      implicit def encoder[E](implicit e: Encoder[E]): Encoder[Response[E]] = deriveEncoder[Response[E]]
    }

    case class Product(modelId: String, sku: String, store: String, ttlInSeconds: Int, metadata: ProductMetadata)

    case class ProductMetadata(manufactured_date: ZonedDateTime, organic: Boolean)
    object ProductMetadata {
      // Boolean codec required - because `organic` is a string in JSON, which has boolean type
      implicit val booleanDecoder: Decoder[Boolean] = Decoder[String].emapTry(value => Try(value.toBoolean))
      implicit val booleanEncoder: Encoder[Boolean] = Encoder[String].contramap(_.toString)

      implicit val decoder: Decoder[ProductMetadata] = deriveDecoder[ProductMetadata]
      implicit def encoder: Encoder[ProductMetadata] = deriveEncoder[ProductMetadata]
    }

    val json =
      s"""
         |{
         |   "requestId":"91ee60d5f1b45e#316",
         |   "error":null,
         |   "errorMessages":[
         |
         |   ],
         |   "entries":[
         |      {
         |         "modelId":"RT001",
         |         "sku":"SKU-ASC001",
         |         "store":"livingstone",
         |         "ttlInSeconds":8000,
         |         "metadata":{
         |            "manufactured_date":"2019-01-22T01:25Z",
         |            "organic":"true"
         |         }
         |      },
         |      {
         |         "modelId":"RT002",
         |         "sku":"SKU-ASC002",
         |         "store":"livingstone",
         |         "ttlInSeconds":8000,
         |         "metadata":{
         |            "manufactured_date":"2019-10-03T01:25Z",
         |            "organic":"false"
         |         }
         |      }
         |   ]
         |}
         |""".stripMargin

    val parseResult: Either[Error, List[String]] =
      for {
        parsedJson <- parse(json)
        response <- parsedJson.as[Response[Product]]
      } yield  {
        response.entries.collect {
          case Product(modelId, _, "livingstone", _, ProductMetadata(_, true)) => modelId
        }
      }
    println(parseResult)
  }

which will produce next result

Right(List(RT001))

Hope this helps!

Upvotes: 1

Related Questions