2010-11-13 8 views
92

Sto utilizzando la build in classe JSON in Scala 2.8 per analizzare il codice JSON. Non voglio utilizzare l'uno o l'altro di Liftweb a causa della riduzione al minimo delle dipendenze.Come analizzare JSON in Scala usando le classi standard di Scala?

Il modo in cui sto facendo sembra troppo imperativo, c'è un modo migliore per farlo?

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] 
} 

risposta

104

Si tratta di una soluzione basata su estrattori che farà il cast di 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))) 

All'inizio del ciclo for ho artificialmente avvolgere il risultato in una lista in modo che se ne ricava un elenco a fine. Quindi nel resto del ciclo for utilizzo il fatto che i generatori (usando <-) e le definizioni di valore (usando =) utilizzeranno i metodi non validi.

(risposta Più a cura di distanza - controllare la cronologia di modifica se siete curiosi)

+0

Mi piace il suo approccio di modifica 2 di dichiarare oggetti con i tipi previsti e un metodo unapply. Se lo pubblichi come risposta separata, voterò. – Steve

+0

Mi dispiace scovare un vecchio post, ma qual è il significato del primo Some (M (mappa)) nel loop? Capisco che la M (mappa) stia estraendo la mappa sulla variabile "mappa", ma per quanto riguarda gli Alcuni? –

+1

@FedericoBonelli, 'JSON.parseFull' restituisce' Option [Any] ', quindi inizia con' List (None) 'o' List (Some (any)) '. Il 'Some' è per la corrispondenza del modello su' Opzione'. – huynhjl

7

ho provato un paio di cose, favorendo il pattern matching, come un modo per evitare la fusione, ma ha avuto problemi con il tipo di cancellazione sui tipi di raccolta.

Il problema principale sembra essere che il tipo completo del risultato di analisi rispecchia la struttura dei dati JSON ed è ingombrante o impossibile da dichiarare completamente. Immagino sia per questo che Qualsiasi venga utilizzato per troncare le definizioni di tipo. Utilizzo di Qualsiasi determina la necessità di eseguire il casting.

Ho violato qualcosa sotto il quale è conciso ma è estremamente specifico per i dati JSON impliciti dal codice nella domanda. Qualcosa di più generale sarebbe più soddisfacente, ma non sono sicuro che sarebbe molto elegante.

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} 
+0

Mi piace l'utente implicito per estrarlo. – Phil

10

Questo è il mio modo di fare la partita modello:

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) 
} 
11

mi piace @ risposta di huynhjl, mi ha portato verso il basso la strada giusta. Tuttavia, non è eccezionale nel gestire le condizioni di errore. Se il nodo desiderato non esiste, si ottiene un'eccezione cast. Ho adattato questo leggermente per fare uso di Option per gestire meglio questo.

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) 
} 

Ovviamente, questo non gestisce gli errori tanto quanto evitarli. Questo produrrà una lista vuota se qualcuno dei nodi json manca. È possibile utilizzare un match per verificare la presenza di un nodo prima di agire ...

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" 
    } 
} 
+1

Penso che CC unapply possa essere notevolmente semplificato in 'def unapply (a: Option [Any]): Option [T] = a.map (_. AsInstanceOf [T])'. – Suma

+0

Scala 2.12 sembra aver bisogno di ';' prima delle righe con '=' in per comprensione. – akauppi

+0

Per me, il codice più in alto non "produce una lista vuota se qualcuno dei nodi json manca", ma ha dato un "MatchError" invece (Scala 2.12). Necessario per avvolgere il per in un blocco try/catch per quello. Qualche idea migliore? – akauppi

2
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))) 
Problemi correlati