Comment analyser JSON dans Scala à l'aide de classes Scala standard?

113

J'utilise la construction dans la classe JSON dans Scala 2.8 pour analyser le code JSON. Je ne veux pas utiliser l'un ou l'autre Liftweb en raison de la minimisation des dépendances.

La façon dont je le fais me semble trop impérative, y a-t-il une meilleure façon de le faire?

import scala.util.parsing.json._
...
val json:Option[Any] = JSON.parseFull(jsonString)
val map:Map[String,Any] = json.get.asInstanceOf[Map[String, Any]]
val languages:List[Any] = map.get("languages").get.asInstanceOf[List[Any]]
languages.foreach( langMap => {
val language:Map[String,Any] = langMap.asInstanceOf[Map[String,Any]]
val name:String = language.get("name").get.asInstanceOf[String]
val isActive:Boolean = language.get("is_active").get.asInstanceOf[Boolean]
val completeness:Double = language.get("completeness").get.asInstanceOf[Double]
}
Phil
la source

Réponses:

130

Il s'agit d'une solution basée sur des extracteurs qui feront le cast de classe:

class CC[T] { def unapply(a:Any):Option[T] = Some(a.asInstanceOf[T]) }

object M extends CC[Map[String, Any]]
object L extends CC[List[Any]]
object S extends CC[String]
object D extends CC[Double]
object B extends CC[Boolean]

val jsonString =
    """
      {
        "languages": [{
            "name": "English",
            "is_active": true,
            "completeness": 2.5
        }, {
            "name": "Latin",
            "is_active": false,
            "completeness": 0.9
        }]
      }
    """.stripMargin

val result = for {
    Some(M(map)) <- List(JSON.parseFull(jsonString))
    L(languages) = map("languages")
    M(language) <- languages
    S(name) = language("name")
    B(active) = language("is_active")
    D(completeness) = language("completeness")
} yield {
    (name, active, completeness)
}

assert( result == List(("English",true,2.5), ("Latin",false,0.9)))

Au début de la boucle for, j'enveloppe artificiellement le résultat dans une liste afin qu'il donne une liste à la fin. Ensuite, dans le reste de la boucle for, j'utilise le fait que les générateurs (en utilisant <-) et les définitions de valeurs (en utilisant= ) utiliseront les méthodes unapply.

(Ancienne réponse modifiée - vérifiez l'historique des modifications si vous êtes curieux)

huynhjl
la source
Désolé de déterrer un ancien message, mais quelle est la signification du premier Some (M (carte)) de la boucle? Je comprends que le M (carte) extrait la carte vers la variable «carte», mais qu'en est-il du Some?
Federico Bonelli
1
@FedericoBonelli, JSON.parseFullretourne Option[Any], donc ça commence par List(None)ou List(Some(any)). Le Someest pour la correspondance de motifs activée Option.
huynhjl
21

Voici comment je fais la correspondance de motif:

val result = JSON.parseFull(jsonStr)
result match {
  // Matches if jsonStr is valid JSON and represents a Map of Strings to Any
  case Some(map: Map[String, Any]) => println(map)
  case None => println("Parsing failed")
  case other => println("Unknown data structure: " + other)
}
Matthias Braun
la source
pouvez-vous donner un exemple de votre jsonStr, il ne fonctionne pas avec l'exemple ci-dessus de jsonStr
priya khokher
Cela vaut peut-être la peine de publier une question sur votre problème. Je n'ai actuellement pas installé Scala sur ma machine, donc je n'ai pas de chaîne JSON prête.
Matthias Braun
12

J'aime la réponse de @ huynhjl, cela m'a conduit sur la bonne voie. Cependant, il n'est pas excellent pour gérer les conditions d'erreur. Si le nœud souhaité n'existe pas, vous obtenez une exception de conversion. J'ai légèrement adapté cela pour l'utiliser Optionpour mieux gérer cela.

class CC[T] {
  def unapply(a:Option[Any]):Option[T] = if (a.isEmpty) {
    None
  } else {
    Some(a.get.asInstanceOf[T])
  }
}

object M extends CC[Map[String, Any]]
object L extends CC[List[Any]]
object S extends CC[String]
object D extends CC[Double]
object B extends CC[Boolean]

for {
  M(map) <- List(JSON.parseFull(jsonString))
  L(languages) = map.get("languages")
  language <- languages
  M(lang) = Some(language)
  S(name) = lang.get("name")
  B(active) = lang.get("is_active")
  D(completeness) = lang.get("completeness")
} yield {
  (name, active, completeness)
}

Bien sûr, cela ne gère pas les erreurs mais les évite. Cela donnera une liste vide si l'un des nœuds json est manquant. Vous pouvez utiliser a matchpour vérifier la présence d'un nœud avant d'agir ...

for {
  M(map) <- Some(JSON.parseFull(jsonString))
} yield {
  map.get("languages") match {
    case L(languages) => {
      for {
        language <- languages
        M(lang) = Some(language)
        S(name) = lang.get("name")
        B(active) = lang.get("is_active")
        D(completeness) = lang.get("completeness")
      } yield {
        (name, active, completeness)
      }        
    }
    case None => "bad json"
  }
}
Murrayju
la source
3
Je pense que CC unapply peut être considérablement simplifié def unapply(a: Option[Any]): Option[T] = a.map(_.asInstanceOf[T]).
Suma
Scala 2.12 semble avoir besoin de ';' avant les lignes avec '=' dans le pour la compréhension.
akauppi
Pour moi, le code le plus haut n'a pas "donné une liste vide si l'un des nœuds json est manquant", mais a donné un à la MatchErrorplace (Scala 2.12). Nécessaire pour envelopper le for dans un bloc try / catch pour cela. Des idées plus sympas?
akauppi
7

J'ai essayé plusieurs choses, en privilégiant la correspondance de motifs pour éviter la diffusion, mais j'ai rencontré des problèmes d'effacement de type sur les types de collection.

Le principal problème semble être que le type complet du résultat d'analyse reflète la structure des données JSON et est soit encombrant, soit impossible à énoncer complètement. Je suppose que c'est pourquoi Any est utilisé pour tronquer les définitions de type. L'utilisation de Any entraîne le besoin de lancer.

J'ai piraté quelque chose ci-dessous qui est concis mais extrêmement spécifique aux données JSON impliquées par le code de la question. Quelque chose de plus général serait plus satisfaisant mais je ne suis pas sûr que ce soit très élégant.

implicit def any2string(a: Any)  = a.toString
implicit def any2boolean(a: Any) = a.asInstanceOf[Boolean]
implicit def any2double(a: Any)  = a.asInstanceOf[Double]

case class Language(name: String, isActive: Boolean, completeness: Double)

val languages = JSON.parseFull(jstr) match {
  case Some(x) => {
    val m = x.asInstanceOf[Map[String, List[Map[String, Any]]]]

    m("languages") map {l => Language(l("name"), l("isActive"), l("completeness"))}
  }
  case None => Nil
}

languages foreach {println}
Don Mackenzie
la source
J'aime que l'utilisateur de l'implicite l'extrait.
Phil
4
val jsonString =
  """
    |{
    | "languages": [{
    |     "name": "English",
    |     "is_active": true,
    |     "completeness": 2.5
    | }, {
    |     "name": "Latin",
    |     "is_active": false,
    |     "completeness": 0.9
    | }]
    |}
  """.stripMargin

val result = JSON.parseFull(jsonString).map {
  case json: Map[String, List[Map[String, Any]]] =>
    json("languages").map(l => (l("name"), l("is_active"), l("completeness")))
}.get

println(result)

assert( result == List(("English", true, 2.5), ("Latin", false, 0.9)) )
Yuriy Tumakha
la source
3
Ceci est obsolète dans la dernière scala, Unbundled. Une idée de comment l'utiliser alors?
Sanket_patil
4

Vous pouvez faire comme ça! Code JSON très facile à analyser: P

package org.sqkb.service.common.bean

import java.text.SimpleDateFormat

import org.json4s
import org.json4s.JValue
import org.json4s.jackson.JsonMethods._
//import org.sqkb.service.common.kit.{IsvCode}

import scala.util.Try

/**
  *
  */
case class Order(log: String) {

  implicit lazy val formats = org.json4s.DefaultFormats

  lazy val json: json4s.JValue = parse(log)

  lazy val create_time: String = (json \ "create_time").extractOrElse("1970-01-01 00:00:00")
  lazy val site_id: String = (json \ "site_id").extractOrElse("")
  lazy val alipay_total_price: Double = (json \ "alipay_total_price").extractOpt[String].filter(_.nonEmpty).getOrElse("0").toDouble
  lazy val gmv: Double = alipay_total_price
  lazy val pub_share_pre_fee: Double = (json \ "pub_share_pre_fee").extractOpt[String].filter(_.nonEmpty).getOrElse("0").toDouble
  lazy val profit: Double = pub_share_pre_fee

  lazy val trade_id: String = (json \ "trade_id").extractOrElse("")
  lazy val unid: Long = Try((json \ "unid").extractOpt[String].filter(_.nonEmpty).get.toLong).getOrElse(0L)
  lazy val cate_id1: Int = (json \ "cate_id").extractOrElse(0)
  lazy val cate_id2: Int = (json \ "subcate_id").extractOrElse(0)
  lazy val cate_id3: Int = (json \ "cate_id3").extractOrElse(0)
  lazy val cate_id4: Int = (json \ "cate_id4").extractOrElse(0)
  lazy val coupon_id: Long = (json \ "coupon_id").extractOrElse(0)

  lazy val platform: Option[String] = Order.siteMap.get(site_id)


  def time_fmt(fmt: String = "yyyy-MM-dd HH:mm:ss"): String = {
    val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    val date = dateFormat.parse(this.create_time)
    new SimpleDateFormat(fmt).format(date)
  }

}
Echo Zeng
la source
2

C'est ainsi que je fais la bibliothèque du combinateur Scala Parser:

import scala.util.parsing.combinator._
class ImprovedJsonParser extends JavaTokenParsers {

  def obj: Parser[Map[String, Any]] =
    "{" ~> repsep(member, ",") <~ "}" ^^ (Map() ++ _)

  def array: Parser[List[Any]] =
    "[" ~> repsep(value, ",") <~ "]"

  def member: Parser[(String, Any)] =
    stringLiteral ~ ":" ~ value ^^ { case name ~ ":" ~ value => (name, value) }

  def value: Parser[Any] = (
    obj
      | array
      | stringLiteral
      | floatingPointNumber ^^ (_.toDouble)
      |"true"
      |"false"
    )

}
object ImprovedJsonParserTest extends ImprovedJsonParser {
  def main(args: Array[String]) {
    val jsonString =
    """
      {
        "languages": [{
            "name": "English",
            "is_active": true,
            "completeness": 2.5
        }, {
            "name": "Latin",
            "is_active": false,
            "completeness": 0.9
        }]
      }
    """.stripMargin


    val result = parseAll(value, jsonString)
    println(result)

  }
}
hmehdi
la source