Convert Json string to map in scala - scala

I have a JSON string, say:
val json = JSONObject(Map("a" -> 1)).toString()
I want to convert this json to map again. I tried:
val map = json.toMap[String, Int]
This gives me the following error:
Error:(46, 25) Cannot prove that Char <:< (String, Int).
val map = json.toMap[String, Int]
^
Error:(46, 25) not enough arguments for method toMap: (implicit ev: <:<[Char,(String, Int)])scala.collection.immutable.Map[String,Int].
Unspecified value parameter ev.
val map = json.toMap[String, Int]
^
What is the correct way of doing this?

val map = json.toMap.asInstanceOf[Map[String, Int]]

Using play-json you can convert Json to Map using validate. Validate returns JsResult which can be JsSuccess(data,path) or JsError(errors). Pattern match to get the map out of it.
If you are using sbt project then add play-json dependency to your project.
build.sbt
libraryDependencies ++= Seq("com.typesafe.play" %% "play-json" % "2.54.")
Scala REPL
scala> import play.api.libs.json._
import play.api.libs.json._
scala> val map = Map("java" -> 1, "scala" -> 2)
map: scala.collection.immutable.Map[String,Int] = Map(java -> 1, scala -> 2)
scala> Json.toJson(map).validate[Map[String, Int]]
res3: play.api.libs.json.JsResult[Map[String,Int]] = JsSuccess(Map(java -> 1, scala -> 2),)
scala> val result = Json.toJson(map).validate[Map[String, Int]]
result: play.api.libs.json.JsResult[Map[String,Int]] = JsSuccess(Map(java -> 1, scala -> 2),)
scala> result match { case JsSuccess(data, _) => data case JsError(errors) => Unit}
res4: Object = Map(java -> 1, scala -> 2)

If this is a one-off then you could do something like this:
val json = JSONObject(Map("a" -> 1)).toString() //> json : String = {"a" : 1}
val map = json.substring(1, json.length - 1).split(":").map(_.trim) match {
case Array(s: String, i: String) => Map(s -> i.toInt)
} //> map : scala.collection.immutable.Map[String,Int] = Map("a" -> 1)
However, if you are doing more with Json then you probably want to explore the use of a library since I think the support for Json in the standard library is limited. See this post for a good overview of available libraries.

Related

Unable to return Map from a scala method

I am trying to return a Map from a scala method as below.
I have two different Maps with some matching keys. I need to find the matching keys between them and pick the values out of them and put them in another Map in the way I wanted. Below is the code I wrote for the aforementioned action.
val common = rdKeys.keySet.intersect(bounds.keySet).toList
val metaColumns = getReadColumns(common, rdKeys, bounds)
def getReadColumns(common:List[String], rdKeys:scala.collection.mutable.Map[String, String], bounds:scala.collection.mutable.Map[String, String]): scala.collection.mutable.Map[String, String] = {
var metaMap = scala.collection.mutable.Map[String, String]
common.map {
c => metaMap += (c -> bounds(c) + "|" + rdKeys(c))
}
metaMap
}
But the method is giving me a compilations error:
Expression of type Seq[(String, String)] => mutable.Map[String, String] doesn't confirm to expected type mutable.Map[String, String]
All the method parameters, Maps used inside the method & the return type of the method are of mutable.Map[String, String]. I don't understand what is the mistake I did here.
Could anyone let me know what do I have to do to correct the problem ?
You got the error
Expression of type Seq[(String, String)] => mutable.Map[String, String] doesn't confirm to expected type mutable.Map[String, String]
Because of the statement scala.collection.mutable.Map[String, String] returns a function Seq[(String, String)] => mutable.Map[String, String]
You can correct it by empty method:
def getReadColumns( common:List[String],
rdKeys:scala.collection.mutable.Map[String, String],
bounds:scala.collection.mutable.Map[String, String]):scala.collection.mutable.Map[String, String] = {
val metaMap = scala.collection.mutable.Map.empty[String, String]
common.foreach {
c => metaMap.update(c, bounds(c) + "|" + rdKeys(c))
}
metaMap
}
P.S. or use c => metaMap += c -> (bounds(c) + "|" + rdKeys(c))
map preserves the collection type. You can map a List to another List, and, in the end, cast your List directly to a Map
val common = List("a", "b")
val rdKeys = Map("a" -> 1, "b" -> 1)
val bounds = Map("a" -> 10, "b" -> 10)
common // this is a list
.map(c => c -> (bounds(c) + "|" + rdKeys(c))) // this is a list
.toMap // then cast to it to a Map
This code outputs
scala.collection.immutable.Map[String,String] = Map(a -> 10|1, b -> 10|1)

How to convert Seq[Column] into a Map[String,String] and change value?

I'm new to Scala and Spark and I'm struggling to do the following,
I'm trying to convert a Seq into a Map and at the same time, modify the value when I'm converting the sequence by adding a "_suffix" like so:
val columns = Seq($"col2",$"col3")
val map = columns.map(t => t.toString() -> t.toString()+"_suffix").toMap
However, I'm getting the following error:
scala> val map = columns.map(t => t.toString() -> t.toString()+"_suffix").toMap
<console>:25: error: Cannot prove that String <:< (T, U).
val map = columns.map(t => t.toString() -> t.toString()+"_suffix").toMap
Furthermore, the .map result returns the following:
scala> val map = columns.map(t => t.toString() -> t.toString()+"_suffix")
map: Seq[String] = List((col2,col2)_suffix, (col3,col3)_suffix)
And this is what I'm trying to produce:
map: Seq[(String, String)] = List((col2,col2_suffix), (col3,col3_suffix)
So I can ultimately convert it into a Map:
map: scala.collection.immutable.Map[String,String] = Map(col2 -> col2_suffix, col3 -> col3_suffix)
I'm pretty stuck trying to achieve this, any advice?
What you have done is correct, just add some parenthesis and it will work:
columns.map(t => t.toString() -> (t.toString() + "_suffix")).toMap
As you can see from your results, the "_suffix" is added to the string of the tuple, i.e.
(col2,col2)_suffix
What you want to do is add the string to the second element only, hence the parenthesis is necessary.
Here is your supposed input($ to be removed before quotes as shown in question):
scala> val columns = Seq("col2","col3")
columns: Seq[String] = List(col2, col3)
In your code there is no need to use toString() method
on each string as they are already strings,
and use map as below:
val map = columns.map(t => (t,t+"_suffix")).toMap
In Scala REPL:
scala> val map = columns.map(t => (t,t+"_suffix"))
map: Seq[(String, String)] = List((col2,col2_suffix), (col3,col3_suffix))
scala> val map = columns.map(t => (t,t+"_suffix")).toMap
map: scala.collection.immutable.Map[String,String] = Map(col2 -> col2_suffix, col3 -> col3_suffix)

wrapping different datatype into a generic, in scala

I expect to return a map containing value of different datatypes such as
(key -> String) and (key -> Int), but i can have Map either of
Map[String,String] or Map[String,Int].
I can't use class because number and order of keys are not fixed.
Is there any way to wrap String and Int to a generic class so that i can return map as Map[String,Any]
You can use HMap as #Harnish suggested, but there is an alternative in the scala library: Map[String, Either[Int, String]]. It applies only if you know that the types either one or another and nothing more.
The type Either[Int, String] can be created either by Left(5) or Right("Hello"). Then you can use match to test the value:
x match {
case Left(n) => println(s"$n is a number")
case Right(s) => println(s"$s is a string")
}
Updated
Example:
val dict = scala.collection.mutable.Map[String, Either[String, Int]]()
dict += ("a" -> Right(5))
dict += ("b" -> Left("Hello"))
dict map {
case (key, Right(n)) => println(s"For $key: $n is integer")
case (key, Left(s)) => println(s"For $key: $s is string")
}
I'm not sure if you can do this with the standard collections library, however it is possible using shapeless HMap (Heterogenous map). This is the example given in the docs, which closely matches what you have described:
// Key/value relation to be enforced: Strings map to Ints and vice versa
class BiMapIS[K, V]
implicit val intToString = new BiMapIS[Int, String]
implicit val stringToInt = new BiMapIS[String, Int]
val hm = HMap[BiMapIS](23 -> "foo", "bar" -> 13)
//val hm2 = HMap[BiMapIS](23 -> "foo", 23 -> 13) // Does not compile
scala> hm.get(23)
res0: Option[String] = Some(foo)
scala> hm.get("bar")
res1: Option[Int] = Some(13)
Note, it doesn't give you an Any, instead you have to specify what is valid in your key/value pairs. I'm not sure if that's helpful to you or not...

How to deserialize a Map of Map with Play

When I execute in the REPL, it works (due to implicit mapWrites in the scope):
scala> Map("a"->1l, "b"->2l)
res0: scala.collection.immutable.Map[String,Long] = Map(a -> 1, b -> 2)
scala> Map("c" -> res0, "d" -> res0)
res1: scala.collection.immutable.Map[String,scala.collection.immutable.Map[String,Long]] = Map(c -> Map(a -> 1, b -> 2), d -> Map(a -> 1, b -> 2))
scala> import play.api.libs.json._
import play.api.libs.json._
scala> Json.toJson(res1)
res2: play.api.libs.json.JsValue = {"c":{"a":1,"b":2},"d":{"a":1,"b":2}}
Why my code still doesn't compile (it's the same type as in the REPL) ?
No Json deserializer found for type Map[String,Map[String,Long]]. Try to implement an implicit Writes or Format for this type.
[edit] I've found a workaround but i don't understand why i need it :
implicit def mapWrites = Writes[Map[String,Map[String,Long]]] ( m => Writes.mapWrites(Writes.mapWrites[Long]).writes(m))
Play 2.1 JSON API does not provide a serializer for the Type Map[String, Object].
Define case class and Format for the specific type instead of Map[String, Object]:
// { "val1" : "xxx", "val2" : ["a", "b", "c"] }
case class Hoge(val1: String, val2: List[String])
implicit val hogeFormat = Json.format[Hoge]
If you don't want to create case class.
The following code provides JSON serializer/deserializer for Map[String, Object]:
implicit val objectMapFormat = new Format[Map[String, Object]] {
def writes(map: Map[String, Object]): JsValue =
Json.obj(
"val1" -> map("val1").asInstanceOf[String],
"val2" -> map("val2").asInstanceOf[List[String]]
)
def reads(jv: JsValue): JsResult[Map[String, Object]] =
JsSuccess(Map("val1" -> (jv \ "val1").as[String], "val2" -> (jv \ "val2").as[List[String]]))
}
Jackson doesn't know how to deserialize the Scala collection class scala.collection.immutable.Map, because it doesn't implement any of the Java collection interfaces.
You can either deserialize to a Java collection:
val mapData = mapper.readValue(jsonContent, classOf[java.util.Map[String,String]])
or add the Scala module to the mapper:
mapper.registerModule(DefaultScalaModule)
val mapData = mapper.readValue(jsonContent, classOf[Map[String,String]])
or you can also try like this
import play.api.libs.json._
val a1=Map("val1"->"a", "val2"->"b")
Json.toJSon(a1)
Because a1 is just Map[String,String] that works OK.
But if I have something more complex like where I have Map[String,Object], that doesn't work:
val a = Map("val1" -> "xxx", "val2"-> List("a", "b", "c"))
Json.toJSon(a1)
>>> error: No Json deserializer found for type scala.collection.immutable.Map[String,Object]
I found that I can do something like the following:
val a2 = Map("val1" -> Json.toJson("a"), "val2" -> Json.toJson(List("a", "b", "c")))
Json.toJson(a2)
And that works.`

Append Map[String, String] to a Seq[Map[String, String]]

This drives me crazy, I can't figure out why this gives me an error.
Here an example of my code:
var seqOfObjects:Seq[Map[String, String]] = Seq[Map[String, String]]()
for(item <- somelist) {
seqOfObjects += Map(
"objectid" -> item(0).toString,
"category" -> item(1),
"name" -> item(2),
"url" -> item(3),
"owneremail" -> item(4),
"number" -> item(5).toString)
}
This gives me an error saying:
Type mismatch, expected: String, actual: Map[String, String]
But a Map[String, String] is exactly what I want to append into my Seq[Map[String, String]].
Why is it saying that my variable seqOfObjects expects a String??
Anyone have a clue?
Thanks
a += b means a = a.+(b). See this answer.
There is no method + in Seq, so you can't use +=.
scala> Seq[Int]() + 1
<console>:8: error: type mismatch;
found : Int(1)
required: String
Seq[Int]() + 1
^
required: String is from string concatenation. This behavior is inherited from Java:
scala> List(1, 2, 3) + "str"
res0: String = List(1, 2, 3)str
Actually method + here is from StringAdd wrapper. See implicit method Predef.any2stringadd.
You could use :+= or +:= instead of +=.
Default implementation of Seq is List, so you should use +: and +:= instead of :+ and :+=. See Performance Characteristics of scala collections.
You could also use List instead of Seq. There is :: method in List, so you can use ::=:
var listOfInts = List[Int]()
listOfInts ::= 1
You can rewrite your code without mutable variables using map:
val seqOfObjects =
for(item <- somelist) // somelist.reverse to reverse order
yield Map(...)
To reverse elements order you could use reverse method.
Short foldLeft example:
sl.foldLeft(Seq[Map[Srting, String]]()){ (acc, item) => Map(/* map from item */) +: acc }