Given the scenario where dictionary would have nested keys as in JSON
let toConvert = dict["Id", "001"; "title", "one"; "payload.subname", "oneone"; "payload.type", "awesome" ]
How can I produce JSON string with nested object, like:
{
"Id": "001",
"title": "one",
"payload": {
"subname": "oneone",
"type": "awesome"
}
}
Any ideas?
First approach
let printArgumentValue argument value =
[
"\"";
argument;
"\": \"";
value;
"\""
] |> String.concat ""
let printDictionary (v:string*seq<KeyValuePair<string,string>>) =
match v |> snd |> Seq.length with
| 0 -> ""
| 1 -> [
printArgumentValue (v |> fst) (v |> snd |> Seq.head).Value;
","
] |> String.concat ""
| _ -> [
"\"";
v |> fst;
"\": { ";
v |> snd |> Seq.map(fun kv -> printArgumentValue (kv.Key.Replace(([ v |> fst; "."] |> String.concat ""), "")) kv.Value) |> String.concat ",";
"}"
] |> String.concat ""
toConvert
|> Seq.groupBy (fun (KeyValue(k,v)) -> k.Split('.').[0])
|> Seq.map(fun v -> printDictionary v)
|> String.concat ""
Now just missing recursion.
Using the Newtonsoft.Json NuGet package, the approach here is to build up the plain values into a JObject with a recursive function.
open Newtonsoft.Json.Linq
let normalise rawDict =
rawDict
|> Seq.map (fun (KeyValue (k, v)) -> ((k:string).Split('.') |> Array.toList), v)
|> Seq.toList
let rec buildJsonObject values : JObject =
values
|> List.groupBy (fun (keys, _) ->
match keys with
| [] | [ _ ] -> None
| childObjectKey :: _ -> Some childObjectKey)
|> List.collect (fun (childObjectKey, values) ->
match childObjectKey with
| None ->
values
|> List.map (function
| [ k ], v -> JProperty(k, JValue (v:string))
| _ -> failwith "unpossible!")
| Some childObjectKey ->
let childObject =
values
|> List.map (fun (keys, v) -> List.tail keys, v)
|> buildJsonObject
[ JProperty(childObjectKey, childObject) ])
|> JObject
Then calling .ToString() via string produces your expected output:
dict ["Id", "001"; "title", "one"; "payload.subname", "oneone"; "payload.type", "awesome" ]
|> normalise
|> buildJsonObject
|> string
Related
I am new to scala and I am trying to parse a JSON shown below
val result = JSON.parseFull("""
{"name": "Naoki", "lang": ["Java", "Scala"] , "positionandvalue": ["5:21", "6:24", "7:6"]}
""")
result: Option[Any] = Some(Map(name -> Naoki, lang -> List(Java, Scala), positionandvalue -> List(5:21, 6:24, 7:6)))
And get the parsed values in a Map
val myMap = result match {
case Some(e) => e
case None => None
}
myMap: Any = Map(name -> Naoki, lang -> List(Java, Scala), positionandvalue -> List(5:21, 6:24, 7:6))
What I need is
1. To get the key as a new variable (to be used as metadata to validate the file) with its corresponding value assigned to it. Something like,
val name = "Naoki"
positionandvalue -> List(5:21, 6:24, 7:6). This variable indicates the List of(Position of string delimited a in file:length of string in position). How can I use this variable to satisfy the requirement.
you cannot dynamically create the variables name and positionandvalue from the Map key. However they can be statically created using the below approach.
val result: Option[Any] = Some(Map("name" -> "Naoki", "lang" -> List("Java", "Scala"), "positionandvalue" -> List("5:21", "6:24", "7:6")))
val myMap: Map[String, Any] = result match {
case Some(e: Map[String, Any] #unchecked) => e
case _ => Map()
}
val name = myMap.get("name") match {
case Some(x: String) => x
case _ => throw new RuntimeException("failure retrieving name key")
}
val positionandvalue = myMap.get("positionandvalue") match {
case Some(x: List[String] #unchecked) => x.map(y => (y.split(":") match {case Array(x1,x2) => x1 -> x2})).toMap
case _ => throw new RuntimeException("failure retrieving positionandvalue key")
}
positionandvalue: scala.collection.immutable.Map[String,String] = Map(5 -> 21, 6 -> 24, 7 -> 6)
I'm making an extension of the Xamarin Forms Map class, which is amenable to MVVM architecture. Here is the derived type:
type GeographicMap() =
inherit Map()
static let centerProperty = BindableProperty.Create("Center", typeof<GeodesicLocation>, typeof<GeographicMap>, new GeodesicLocation())
static let radiusProperty = BindableProperty.Create("Radius", typeof<float>, typeof<GeographicMap>, 1.0)
member this.Radius
with get() = 1.0<km> * (this.GetValue(radiusProperty) :?> float)
and set(value: float<km>) = if not <| value.Equals(this.Radius) then this.SetValue(radiusProperty, value / 1.0<km>)
member this.Center
with get() = this.GetValue(centerProperty) :?> GeodesicLocation
and set(value: GeodesicLocation) = if not <| value.Equals(this.Center) then this.SetValue(centerProperty, value)
override this.OnPropertyChanged(propertyName) =
match propertyName with
| "VisibleRegion" ->
this.Center <- this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation
this.Radius <- this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
| "Radius" | "Center" ->
match box this.VisibleRegion with
| null -> this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
| _ ->
let existingCenter, existingRadius = this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation, this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
let deltaCenter, deltaRadius = Geodesic.WGS84.Distance existingCenter (this.Center), existingRadius - this.Radius
let threshold = 0.1 * this.Radius
if Math.Abs(deltaRadius / 1.0<km>) > threshold / 1.0<km> || Math.Abs((deltaCenter |> UnitConversion.kilometres) / 1.0<km>) > threshold / 1.0<km> then
this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
| _ -> propertyName |> ignore
In my view, I have added a binding between the Center property and my ViewModel's Location property as follows:
type DashboardView(theme: Theme) as this =
inherit ContentPage<DashboardViewModel, DashboardView>(theme)
new() = new DashboardView(Themes.AstridTheme)
override __.CreateContent() =
theme.GenerateGrid([|"Auto"; "*"|], [|"*"|]) |> withColumn(
[|
theme.VerticalLayout() |> withBlocks(
[|
theme.GenerateLabel(fun l -> this.Title <- l)
|> withAlignment LayoutOptions.Center LayoutOptions.Center
|> withOneWayBinding(this.ViewModel, this, <# fun (vm: DashboardViewModel) -> vm.Title #>, <# fun (v: DashboardView) -> (v.Title: Label).Text #>)
theme.GenerateSearchBar(fun sb -> this.AddressSearchBar <- sb)
|> withSearchBarPlaceholder LocalisedStrings.SearchForAPlaceOfInterest
|> withTwoWayBinding(this.ViewModel, this, <# fun (vm: DashboardViewModel) -> vm.SearchAddress #>, <# fun (v: DashboardView) -> (v.AddressSearchBar: SearchBar).Text #>)
|> withSearchCommand this.ViewModel.SearchForAddress
|])
theme.GenerateMap(fun m -> this.Map <- m)
|> withTwoWayBinding(this.ViewModel, this, <# fun (vm: DashboardViewModel) -> vm.Location #>, <# fun (v:DashboardView) -> (v.Map: GeographicMap).Center #>)
|]) |> createFromColumns :> View
member val AddressSearchBar = Unchecked.defaultof<SearchBar> with get, set
member val Title = Unchecked.defaultof<Label> with get, set
member val Map = Unchecked.defaultof<GeographicMap> with get, set
Notice that I have a two-way binding between DashboardViewModel.Location and DashboardView.Map.Center. I also have a two-way binding between DashboardViewModel.SearchAddress and DashboardView.AddressSearchBar.Text. The latter binding works; the former does not. I assume this must be because I have not set up the bindable property GeographicMap.Center correctly.
I know that the two-way binding isn't working because panning the map causes the VisibleRegion property to get modified, which in turn triggers an update of the Center property. However, in my ViewModel class:
type DashboardViewModel(?host: IScreen, ?platform: IPlatform) as this =
inherit ReactiveViewModel()
let host, platform = LocateIfNone host, LocateIfNone platform
let searchResults = new ObservableCollection<GeodesicLocation>()
let commandSubscriptions = new CompositeDisposable()
let geocodeAddress(vm: DashboardViewModel) =
let vm = match box vm with | null -> this | _ -> vm
searchResults.Clear()
async {
let! results = platform.Geocoder.GetPositionsForAddressAsync(vm.SearchAddress) |> Async.AwaitTask
results |> Seq.map (fun r -> new GeodesicLocation(r.Latitude * 1.0<deg>, r.Longitude * 1.0<deg>)) |> Seq.iter searchResults.Add
match results |> Seq.tryLast with
| Some position -> return position |> XamarinGeographic.geodesicLocation |> Some
| None -> return None
} |> Async.StartAsTask
let searchForAddress = ReactiveCommand.CreateFromTask geocodeAddress
let mutable searchAddress = String.Empty
let mutable location = new GeodesicLocation(51.4<deg>, 0.02<deg>)
override this.SubscribeToCommands() = searchForAddress.ObserveOn(RxApp.MainThreadScheduler).Subscribe(fun res -> match res with | Some l -> this.Location <- l | None -> res |> ignore) |> commandSubscriptions.Add
override __.UnsubscribeFromCommands() = commandSubscriptions.Clear()
member __.Title with get() = LocalisedStrings.AppTitle
member __.SearchForAddress with get() = searchForAddress
member this.SearchAddress
with get() = searchAddress
// GETS HIT WHEN SEARCH TEXT CHANGES
and set(value) = this.RaiseAndSetIfChanged(&searchAddress, value, "SearchAddress") |> ignore
member this.Location
with get() = location
// DOES NOT GET HIT WHEN THE MAP GETS PANNED, TRIGGERING AN UPDATE OF ITS Center PROPERTY
and set(value) = this.RaiseAndSetIfChanged(&location, value, "Location") |> ignore
interface IRoutableViewModel with
member __.HostScreen = host
member __.UrlPathSegment = "Dashboard"
the SearchAddress setter gets hit whenever the search text is updated, while the Location setter does not get hit when the map is panned, causing an update of its Center property.
Am I missing something relating to my setup of the bindable Center property?
UPDATE: This is something to do with ReactiveUI's WhenAnyValue extension, which is used internally in my binding. To demonstrate this, I added a couple of lines to the View creation:
override __.CreateContent() =
let result =
theme.GenerateGrid([|"Auto"; "*"|], [|"*"|]) |> withColumn(
[|
theme.VerticalLayout() |> withBlocks(
[|
theme.GenerateLabel(fun l -> this.Title <- l)
|> withAlignment LayoutOptions.Center LayoutOptions.Center
|> withOneWayBinding(this.ViewModel, this, <# fun (vm: DashboardViewModel) -> vm.Title #>, <# fun (v: DashboardView) -> (v.Title: Label).Text #>)
theme.GenerateSearchBar(fun sb -> this.AddressSearchBar <- sb)
|> withSearchBarPlaceholder LocalisedStrings.SearchForAPlaceOfInterest
|> withTwoWayBinding(this.ViewModel, this, <# fun (vm: DashboardViewModel) -> vm.SearchAddress #>, <# fun (v: DashboardView) -> (v.AddressSearchBar: SearchBar).Text #>)
|> withSearchCommand this.ViewModel.SearchForAddress
|])
theme.GenerateMap(fun m -> this.Map <- m)
|> withTwoWayBinding(this.ViewModel, this, <# fun (vm: DashboardViewModel) -> vm.Location #>, <# fun (v:DashboardView) -> (v.Map: GeographicMap).Center #>)
|]) |> createFromColumns :> View
this.WhenAnyValue(ExpressionConversion.toLinq <# fun (v:DashboardView) -> (v.Map: GeographicMap).Center #>).ObserveOn(RxApp.MainThreadScheduler).Subscribe(fun (z) ->
z |> ignore) |> ignore // This breakpoint doesn't get hit when the map pans.
this.WhenAnyValue(ExpressionConversion.toLinq <# fun (v:DashboardView) -> (v.AddressSearchBar: SearchBar).Text #>).ObserveOn(RxApp.MainThreadScheduler).Subscribe(fun (z) ->
z |> ignore) |> ignore // This breakpoint gets hit when text is changed in the search bar.
result
You should not make any other operations rather than GetValue() and SetValue() calls in your BindableProperty's get and set definitions. In order to make additional changes when this property is set or changed, you can override OnPropertyChanged method and make necessary operations there.
The solution was very simple.
I was overriding OnPropertyChanged without calling down to the base implementation, which fires the public PropertyChanged event:
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
So what I needed to do was to add a call to base.OnPropertyChanged() to my override:
type GeographicMap() =
inherit Map()
static let centerProperty = BindableProperty.Create("Center", typeof<GeodesicLocation>, typeof<GeographicMap>, new GeodesicLocation())
static let radiusProperty = BindableProperty.Create("Radius", typeof<float>, typeof<GeographicMap>, 1.0)
member this.Radius
with get() = 1.0<km> * (this.GetValue(radiusProperty) :?> float)
and set(value: float<km>) = if not <| value.Equals(this.Radius) then this.SetValue(radiusProperty, value / 1.0<km>)
member this.Center
with get() = this.GetValue(centerProperty) :?> GeodesicLocation
and set(value: GeodesicLocation) = if not <| value.Equals(this.Center) then this.SetValue(centerProperty, value)
override this.OnPropertyChanged(propertyName) =
base.OnPropertyChanged(propertyName)
match propertyName with
| "VisibleRegion" ->
this.Center <- this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation
this.Radius <- this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
| "Radius" | "Center" ->
match box this.VisibleRegion with
| null -> this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
| _ ->
let existingCenter, existingRadius = this.VisibleRegion.Center |> XamarinGeographic.geodesicLocation, this.VisibleRegion.Radius |> XamarinGeographic.geographicDistance
let deltaCenter, deltaRadius = Geodesic.WGS84.Distance existingCenter (this.Center), existingRadius - this.Radius
let threshold = 0.1 * this.Radius
if Math.Abs(deltaRadius / 1.0<km>) > threshold / 1.0<km> || Math.Abs((deltaCenter |> UnitConversion.kilometres) / 1.0<km>) > threshold / 1.0<km> then
this.MoveToRegion(MapSpan.FromCenterAndRadius(this.Center |> XamarinGeographic.position, this.Radius |> XamarinGeographic.distance))
| _ -> propertyName |> ignore
This change allows the public event to fire. This event is translated into an IObservable by ReactiveUI using Observable.FromEventPattern.
I'm new to Scala. How do I handle the JsNull value in my code?
I'm using json4s to convert the JSON to a map.
Should I somehow be converting JsNull to an Option?
Example:
Play JSON : creating json
val jsonA: JsValue = Json.obj(
"name" -> "Bob",
"location" -> "Irvine",
"resident" -> "No",
"nick-name" -> "Bigwig",
"age" -> "6",
"role" -> JsNull,
"car" -> "BMW",
"multiple-residents" -> JsArray(Seq(
JsObject(Seq(
"name" -> JsString("Fiver"),
"age" -> JsNumber(4),
"role" -> JsObject(Seq(
"position" -> JsString("Fiver"),
"" -> JsNumber(4),
"role" -> JsString("janitor")
))
))
))
)
json4s : parsing the json
var jsonAMap:Map[String, Any] = Map()
val jsonAString: String = Json.stringify(jsonA)
jsonAMap = jsonStrToMap(jsonAString)
After the JsValue is converted to a Map using json4s it looks like this:
Map(name -> Bob, location -> Irvine, role -> null, resident -> No, car -> BMW, multiple-residents -> List(Map(name -> Fiver, age -> 4, role -> Map(position -> Fiver, -> 4, role -> janitor))), age -> 6, nick-name -> Bigwig)
When I create a create a List of the values, I end up with a null value in my list. Once I'm pattern matching all the values of the list I end up trying to patter match a null which is not possible (I'm sure I'm not supposed to use a will card for all my cases, but I'm learning) :
for(i <- 0 to beforeValsList.length - 1){
beforeValsList(i) match {
case _ : Map[_,_] =>
compareJson(
beforeValsList(i).asInstanceOf[Map[String,Any]],
afterValsList(i).asInstanceOf[Map[String,Any]],
rdeltaBefore, rdeltaAfter, sameKeyList(i).toString()
)
case _ if (beforeValsList(i) != afterValsList(i)) =>
// if i'm from a recursion, build a new map and add me
// to the deltas as a key->value pair
rdeltaBefore += sameKeyList(i).toString -> beforeValsList(i)
rdeltaAfter += sameKeyList(i).toString -> afterValsList(i)
case _ =>
println("catch all: " + beforeValsList(i).toString
+ " " + afterValsList(i).toString)
}
}
json4s converts JsNull to a null. Should I do a null check:
if(!beforeValsList(i) == null){
beforeValsList(i) match{...}
}
Or is there a way for me to change the null to an Option when I'm putting the values from the Map to a List?
I'm not sure what best practices are and why jsno4s changes JsNull to null instead of an Option, and whether or not that's possible.
Cheers.
I'm still not sure how you would like to handle JsNull, null (or None if you use Option), but you function to get the difference between the Map[String, Any] before and after can be simplified :
type JsonMap = Map[String, Any]
def getMapDiffs(mapBefore: JsonMap, mapAfter: JsonMap) : (JsonMap, JsonMap) = {
val sameKeys = mapBefore.keySet intersect mapAfter.keySet
val startAcc = (Map.empty[String, Any], Map.empty[String, Any])
sameKeys.foldLeft(startAcc){ case (acc # (deltaBefore, deltaAfter), key) =>
(mapBefore(key), mapAfter(key)) match {
// two maps -> add map diff recursively to before diff and after diff
case (beforeMap: Map[_, _], afterMap: Map[_, _]) =>
val (deltaB, deltaA) =
getMapDiffs(beforeMap.asInstanceOf[JsonMap], afterMap.asInstanceOf[JsonMap])
(deltaBefore + (key -> deltaB), deltaAfter + (key -> deltaA))
// values before and after are different
// add values to before diff and after diff
case (beforeValue, afterValue) if beforeValue != afterValue =>
(deltaBefore + (key -> beforeValue), deltaAfter + (key -> afterValue))
// keep existing diff
case _ => acc
}
}
}
Which can be used as:
val (mapBefore, mapAfter) = (
Map("a" -> "alpha", "b" -> "beta", "c" -> "gamma", "d" -> Map("e" -> "epsilon")),
Map("a" -> "alpha", "b" -> List("beta"), "c" -> null, "d" -> Map("e" -> 3))
)
val (deltaBefore, deltaAfter) = getMapDiffs(mapBefore, mapAfter)
// deltaBefore: JsonMap = Map(b -> beta, c -> gamma, d -> Map(e -> epsilon))
// deltaAfter: JsonMap = Map(b -> List(beta), c -> null, d -> Map(e -> 3))
deltaBefore.toList
// List[(String, Any)] = List((b,beta), (c,gamma), (d,Map(e -> epsilon)))
I've got the following functions to decode JSON files using the Data.Aeson library:
data SearchResult = SearchResult {
items :: [Item]
} deriving (Show)
instance FromJSON SearchResult where
parseJSON :: Value -> Parser SearchResult
parseJSON (Object v) = SearchResult <$>
parseJSON (fromJust $ HM.lookup "items" v)
parseJSON _ = mzero
data Item = Item {
volumeInfo :: VolumeInfo
} deriving (Show)
instance FromJSON Item where
parseJSON :: Value -> Parser Item
parseJSON (Object v) = Item <$>
parseJSON (fromJust $ HM.lookup "volumeInfo" v)
parseJSON _ = mzero
data VolumeInfo = VolumeInfo {
title :: String,
authors :: [String],
publisher :: String,
publishedDate :: String,
industryIdentifiers :: [IndustryIdentifier],
pageCount :: Int,
categories :: [String]
} deriving (Show)
instance FromJSON VolumeInfo where
parseJSON :: Value -> Parser VolumeInfo
parseJSON (Object v) = VolumeInfo <$>
v .: "title" <*>
v .: "authors" <*>
v .: "publisher" <*>
v .: "publishedDate" <*>
parseJSON (fromJust $ HM.lookup "industryIdentifiers" v) <*>
v .: "pageCount" <*>
v .: "categories"
parseJSON _ = mzero
data IndustryIdentifier = IndustryIdentifier {
identifierType :: String,
identifier :: String
} deriving (Show)
instance FromJSON IndustryIdentifier where
parseJSON :: Value -> Parser IndustryIdentifier
parseJSON (Object v) = IndustryIdentifier <$>
v .: "type" <*>
v .: "identifier"
parseJSON _ = mzero
And this function:
getBook content = do
putStrLn (Data.ByteString.Lazy.Char8.unpack content)
let searchResult = decode content :: Maybe SearchResult
print (isNothing searchResult)
print searchResult
The function getBook works with many JSON files. Here's an example:
False
Just (SearchResult {items = [Item {volumeInfo = VolumeInfo {title = "A Memoir of Jane Austen", authors = ["James Edward Austen-Leigh","Jane Austen, James Austen-Leigh"], publisher = "Wordsworth Editions", publishedDate = "2007", industryIdentifiers = [IndustryIdentifier {identifierType = "ISBN_10", identifier = "1840225602"},IndustryIdentifier {identifierType = "ISBN_13", identifier = "9781840225600"}], pageCount = 256, categories = ["Novelists, English"]}}]})
The JSON content was successfully decoded, and therefore isNothing returns False in the first line, followed by the decoded content. If I run the function again with this JSON file as content, I get the following output:
True
Nothing
The file couldn't be decoded (as there is no field categories on the JSON file), and so isNothing returns True, and Nothing is printed on screen. Now the problem is when I run it with this JSON file as content. I get this:
*** Exception: Maybe.fromJust: Nothing
An exception is thrown when print (isNothing searchResult) is executed, and I don't understand why True isn't returned like in the previous example (because in this case there is no field industryIdentifiers, for example). What am I missing or doing wrong?
EDIT:
I found out the problem happens every time the JSON file doesn't include the field industryIdentifiers. It fails in this line:
parseJSON (fromJust $ HM.lookup "industryIdentifiers" v) <*>
The github package defines a convenience operator to define array fields:
-- | A slightly more generic version of Aeson's #(.:?)#, using `mzero' instead
-- of `Nothing'.
(.:<) :: (FromJSON a) => Object -> T.Text -> Parser [a]
obj .:< key = case Map.lookup key obj of
Nothing -> pure mzero
Just v -> parseJSON v
(here Map is an alias for Data.HashMap.Lazy)
Then the FromJSON instance of VolumeInfo would be defined like this:
instance FromJSON VolumeInfo
v .: "title" <*>
...
v .:< "industryIdentifiers" <*>
...
I'm trying to convert Scala to JSON in the 2.1RC Play Framework.
I can do the following and get JSON:
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.
But how can I do that in a general way? I thought that I could do something like the following:
a.map{ case(k,v)=> (k, Json.toJson(v) )}
>>> error: No Json deserializer found for type Object
But I still get an error that it can't be deserialized
Additional Information:
Json.toJson can convert a Map[String, String] to a JsValue:
scala> val b = Map( "1" -> "A", "2" -> "B", "3" -> "C", "4" -> "D" )
b: scala.collection.immutable.Map[String,String] = Map(1 -> A, 2 -> B, 3 -> C, 4 -> D)
scala> Json.toJson(b)
res31: play.api.libs.json.JsValue = {"1":"A","2":"B","3":"C","4":"D"}
But, it fails in trying to convert a Map[String, Object]:
scala> a
res34: scala.collection.immutable.Map[String,Object] = Map(val1 -> xxx, val2 -> List(a, b, c))
scala> Json.toJson(a)
<console>:12: error: No Json deserializer found for type scala.collection.immutable.Map[String,Object]. Try to implement an implicit Writes or Format for this type.
Json.toJson(a)
Using the 'hint' from this Play Framework page on converting Scala to Json, I found the following (http://www.playframework.org/documentation/2.0.1/ScalaJson):
If instead of Map[String, Object], there is a Map[String, JsValue], then Json.toJson() will work:
scala> val c = Map("aa" -> Json.toJson("xxxx"), "bb" -> Json.toJson( List("11", "22", "33") ) )
c: scala.collection.immutable.Map[String,play.api.libs.json.JsValue] = Map(aa -> "xxxx", bb -> ["11","22","33"])
scala> Json.toJson(c)
res36: play.api.libs.json.JsValue = {"aa":"xxxx","bb":["11","22","33"]}
So, what I would like, is that given a Map[String, Object], where I know that the Object values were all originally of type String or List[String], how to apply the function Json.toJson() to the all the values in the map and get a Map[String, JsValue].
I also found that I can filter out those values that are purely string and those that are (were) of type List[String]:
scala> val a1 = a.filter({case(k,v) => v.isInstanceOf[String]})
a1: scala.collection.immutable.Map[String,Object] = Map(val1 -> xxx)
scala> val a2 = a.filter({case(k,v) => v.isInstanceOf[List[String]]})
<console>:11: warning: non-variable type argument String in type List[String] is unchecked since it is eliminated by erasure
val a2 = a.filter({case(k,v) => v.isInstanceOf[List[String]]})
^
a2: scala.collection.immutable.Map[String,Object] = Map(val2 -> List(a, b, c))
The List[String] filtering gives a warning, but seems to give the answer I want. If the two filters could be applied and then Json.toJson() used on the values of the result, and the results combined, maybe that would work?
But the filtered results are still of type Map[String, Object] which causes a problem:
scala> Json.toJson(a1)
<console>:13: error: No Json deserializer found for type scala.collection.immutable.Map[String,Object]. Try to implement an implicit Writes or Format for this type.
Json.toJson(a1)
Play 2.1 JSON API does not provide a serializer for the Type Map[String, Ojbect].
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]]))
}
More dynamically
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.json.Json.JsValueWrapper
implicit val objectMapFormat = new Format[Map[String, Object]] {
def writes(map: Map[String, Object]): JsValue =
Json.obj(map.map{case (s, o) =>
val ret:(String, JsValueWrapper) = o match {
case _:String => s -> JsString(o.asInstanceOf[String])
case _ => s -> JsArray(o.asInstanceOf[List[String]].map(JsString(_)))
}
ret
}.toSeq:_*)
def reads(jv: JsValue): JsResult[Map[String, Object]] =
JsSuccess(jv.as[Map[String, JsValue]].map{case (k, v) =>
k -> (v match {
case s:JsString => s.as[String]
case l => l.as[List[String]]
})
})
}
Sample code:
val jv = Json.toJson(Map("val1" -> "xxx", "val2" -> List("a", "b", "c"), "val3" -> "sss", "val4" -> List("d", "e", "f")))
println(jv)
val jr = Json.fromJson[Map[String, Object]](jv)
println(jr.get)
The output:
> {"val1":"xxx","val2":["a","b","c"],"val3":"sss","val4":["d","e","f"]}
> Map(val1 -> xxx, val2 -> List(a, b, c), val3 -> sss, val4 -> List(d, e, f))