get nested fields of Scala map - json

I have the following Map Object :
val ad = node.asInstanceOf[Map[String, Any]]
and a printed example of ad object is:
ListMap(userId -> 1234, userName -> Jason, location -> ListMap(longitude -> -79.234264, latitude -> 37.2395), email -> Some(jason#yahoo.com))
I am trying to access the nested fields of the location field and cast it as a double. I have the following:
ad.get("location") match {
case Some(i) => i match {
case j: Map[Any, Any] => j("longitude").asInstanceOf[Double]
}
My question is there another more graceful/cleaner syntax way to get the nested objects of the location field?

It depends on the concept of "graceful/cleaner" because that kind of data structure doesn't smell good.
Anyway, the inner pattern match can be merged with the outer one, like this:
ad.get("location") match {
case Some(i: Map[Any,Any]) => i("longitude").asInstanceOf[Double]
case _ => // do nothing
}

You can't really do anything graceful with a type like Map[String, Any]. I would consider using a more type-safe Json library in the first place, such as Circe.
Otherwise, it depends on what you know statically about your Map. If you know for certain that "location" exists in the Map, and that it always contains a nested Map with the "longitude" key, and that "longitude" is indeed always a Double, then it's OK to return a Double. Otherwise, you should consider returning an Option[Double] or something like Either[String, Double]. For example:
ad.get("location").flatMap {
case location: Map[String, _] => Try(location("longitude").asInstanceOf[Double]).toOption
case _ => None
}
This will return a None if the desired value cannot be obtained for whatever reason: "location" doesn't exist, "location" is not a map, the "location" map does not contain "longitude", etc.

Related

Kotlinx.Serializer - Create a quick JSON to send

I've been playing with Kotlinx.serialisation. I've been trying to find a quick way to use Kotlinx.serialisation to create a plain simple JSON (mostly to send it away), with minimum code clutter.
For a simple string such as:
{"Album": "Foxtrot", "Year": 1972}
I've been doing is something like:
val str:String = Json.stringify(mapOf(
"Album" to JsonPrimitive("Foxtrot"),
"Year" to JsonPrimitive(1972)))
Which is far from being nice. My elements are mostly primitive, so I wish I had something like:
val str:String = Json.stringify(mapOf(
"Album" to "Sergeant Pepper",
"Year" to 1967))
Furthermore, I'd be glad to have a solution with a nested JSON. Something like:
Json.stringify(JsonObject("Movies", JsonArray(
JsonObject("Name" to "Johnny English 3", "Rate" to 8),
JsonObject("Name" to "Grease", "Rate" to 1))))
That would produce:
{
"Movies": [
{
"Name":"Johnny English 3",
"Rate":8
},
{
"Name":"Grease",
"Rate":1
}
]
}
(not necessarily prettified, even better not)
Is there anything like that?
Note: It's important to use a serialiser, and not a direct string such as
"""{"Name":$name, "Val": $year}"""
because it's unsafe to concat strings. Any illegal char might disintegrate the JSON! I don't want to deal with escaping illegal chars :-(
Thanks
Does this set of extension methods give you what you want?
#ImplicitReflectionSerializer
fun Map<*, *>.toJson() = Json.stringify(toJsonObject())
#ImplicitReflectionSerializer
fun Map<*, *>.toJsonObject(): JsonObject = JsonObject(map {
it.key.toString() to it.value.toJsonElement()
}.toMap())
#ImplicitReflectionSerializer
fun Any?.toJsonElement(): JsonElement = when (this) {
null -> JsonNull
is Number -> JsonPrimitive(this)
is String -> JsonPrimitive(this)
is Boolean -> JsonPrimitive(this)
is Map<*, *> -> this.toJsonObject()
is Iterable<*> -> JsonArray(this.map { it.toJsonElement() })
is Array<*> -> JsonArray(this.map { it.toJsonElement() })
else -> JsonPrimitive(this.toString()) // Or throw some "unsupported" exception?
}
This allows you to pass in a Map with various types of keys/values in it, and get back a JSON representation of it. In the map, each value can be a primitive (string, number or boolean), null, another map (representing a child node in the JSON), or an array or collection of any of the above.
You can call it as follows:
val json = mapOf(
"Album" to "Sergeant Pepper",
"Year" to 1967,
"TestNullValue" to null,
"Musicians" to mapOf(
"John" to arrayOf("Guitar", "Vocals"),
"Paul" to arrayOf("Bass", "Guitar", "Vocals"),
"George" to arrayOf("Guitar", "Sitar", "Vocals"),
"Ringo" to arrayOf("Drums")
)
).toJson()
This returns the following JSON, not prettified, as you wanted:
{"Album":"Sergeant Pepper","Year":1967,"TestNullValue":null,"Musicians":{"John":["Guitar","Vocals"],"Paul":["Bass","Guitar","Vocals"],"George":["Guitar","Sitar","Vocals"],"Ringo":["Drums"]}}
You probably also want to add handling for some other types, e.g. dates.
But can I just check that you want to manually build up JSON in code this way rather than creating data classes for all your JSON structures and serializing them that way? I think that is generally the more standard way of handling this kind of stuff. Though maybe your use case does not allow that.
It's also worth noting that the code has to use the ImplicitReflectionSerializer annotation, as it's using reflection to figure out which serializer to use for each bit. This is still experimental functionality which might change in future.

How do I unpack a JSON value into a tagged union type?

I've got some JSON coming from firebase that looks like this
{
"type": "added",
"doc": {
"id": "asda98j1234jknkj3n",
"data": {
"title": "Foo",
"subtitle": "Baz"
}
}
}
Type can be one of "added", "modified" or "removed". Doc contains an id and a data field. The data field can be any shape and I am able to decode that properly.
I want to use union types to represent these values like so,
type alias Doc data =
(String, data)
type DocChange doc
= Added doc
| Modified doc
| Removed doc
Here the Doc type alias represents the value contained in the doc field in the JSON above. DocChange represents the whole thing. If the type is say "added", then the JSON must decode into Added doc and so on. I don't understand how to decode union types.
I think the andThen function from Json.Decode looks like what I need, but I am unable to use it correctly.
First of all, it seems like you want to constrain the doc parameter of DocChange to a Doc, so you should probably define it like this instead:
type DocChange data
= Added (Doc data)
| Modified (Doc data)
| Removed (Doc data)
Otherwise you'll have to repeatedly specify DocChange (Doc data) in your functions type annotations which quickly gets annoying, and worse the more you nest it. In any case, I've continued using the types as you defined them:
decodeDocData : Decoder DocData
decodeDocData =
map2 DocData
(field "title" string)
(field "subtitle" string)
decodeDoc : Decoder data -> Decoder (Doc data)
decodeDoc dataDecoder =
map2 Tuple.pair
(field "id" string)
(field "data" dataDecoder)
decodeDocChange : Decoder data -> Decoder (DocChange (Doc data))
decodeDocChange dataDecoder =
field "type" string
|> andThen
(\typ ->
case typ of
"added" ->
map Added
(field "doc" (decodeDoc dataDecoder))
"modified" ->
map Modified
(field "doc" (decodeDoc dataDecoder))
"removed" ->
map Removed
(field "doc" (decodeDoc dataDecoder))
_ ->
fail ("Unknown DocChange type: " ++ typ)
)
The trick here is to decode "type" first, then use andThen to switch on it and choose the appropriate decoder. In this case the shape is identical across "types", but it may not be and this pattern gives the flexibility to handle diverging shapes as well. It could be simplified to just selecting the constructor and keeping the rest of the decoding common if you're absolutely sure they won't diverge.

Elm decode json object with array

Im totally new in ELM. Im trying to get data from get response.
Now im trying to make decoder for that. I have json like this:
{
data: [
{
"price" = 300.5
},
{
"price" = 1005
}
]
}
All i need is get lowest price and return it. (At least return each price).
Now i stacked on Decode.index.
priceDecoder : Decode.Decoder String
priceDecoder =
Decode.field "data" Decode.list
What should i do next?
There are several problems with the question itself. First, you're not posting the error you get. The error I get is this:
The 2nd argument to `field` is not what I expect:
25| Decode.field "data" Decode.list
^^^^^^^^^^^
This `list` value is a:
Decode.Decoder a -> Decode.Decoder (List a)
But `field` needs the 2nd argument to be:
Decode.Decoder a
Second, the JSON you've posted is not valid, data should be enclosed in quotes and properties and values should be separated by :, not =.
Thirdly, the type of priceDecoder seems wrong since the JSON contains no String data, or you're not specifying that you also want to convert the Float to a String. I will assume the type is just wrong.
So, the error you (or at least I) get says that list is a function Decoder a -> Decoder (List a), while it expects just a Decoder a value. This is because list expects a Decoder a to be passed to it, which it will use to decode each item in the list.
The decoder we'll use is Decode.field "price" Decode.float, which will decode the "price" field of the object as a Float.
I'll also change the type of priceDecoder from Decoder String to Decoder (List Float), since the price is a Float and we're decoding a List of them, not just getting the first or last value or something like that. I assume that's what you want since you say "at least return each price'.
The priceDecoder we get then is:
priceDecoder : Decode.Decoder (List Float)
priceDecoder =
Decode.field "data" (Decode.list (Decode.field "price" Decode.float))

Apply key to map obtained via pattern matching in Scala (type erased)

I am trying to query an API which returns a JSON array (e.g. [{"name":"obj1", "value":5}, {"name":"obj2", "value":2}]) and process the result, which gets parsed as an Option[List[Map[String,Any]]]. However, I am not sure how to properly extract each Map, since the types are erased at runtime.
import scala.util.parsing.json._
import scalaj.http._
val url = "https://api.github.com/users/torvalds/repos"
val req = Http(url).asString
val parsed = JSON.parseFull(req.body) match {
case Some(data) => data match {
case list: List[_] => list
case _ => sys.error("Result is not a list.")
}
case None => sys.error("Invalid JSON received.")
}
parsed.foreach{
case x: Map[_,_] => x.get("full_name") // error here
}
The error occurs because I cannot apply the function with a String key type. However, because of type erasure, the key and value type are unknown, and specifying that it's a String map throws compiler warnings.
Am I going about things the wrong way? Or maybe I'd have better luck with a different HTTP/JSON library?
You can replace your last line with:
parsed.collect{ case x: Map[_,_] => x.asInstanceOf[Map[String,Any]].get("full_name") }
We sort of "cheat" here since we know the keys in a JSON are always Strings.
As for your last question, if you need something lightweight, I think what you have here is as simple as it gets.
Take a look at this SO post if you want to do something more powerful with your pattern matching.

error: the type of this value must be known in this context (Rust) / serde_json Value

I am using serde_json to deserialise a json document. I have a function that given a string (this is the json document), will return a serde_json Value (this is an enum that represents the json type), returns an Option.
This value is passed around to other functions as required.
However, I realised that passing around a Value is not quite what I want, because doing this, the key is not available.
To illustrate my point, if I have a json document that looks like this:
{
"root" : {
"regex" : null,
"prefixes" : [ "a_", "b_" ]
}
}
"root" is a json object, "regex" is json Null and "prefixes" is a json array.
Now, the json type Value is an enum with discriminators representing the json types, eg Object, Null, Array for the examples given above.
The serde_json crate uses std::collections::BTreeMap to represent nodes in the json document, where the String type repesents the json keys (in the above, these would be "root", "regex" and "prefixes". So passing around just references to Values is only partly helpful, I should be passing around BTreeMap instead, so that I can access the key too.
So this is the following function that I am trying to re-write:
fn get_json_content(content_s : &str) -> Option<Value> {
// instead of returning a value, we need to return a BTreeMap, so we can get the
// key and the value.
println!("===>>> json_content obtained: {}", content_s);
match serde_json::from_str(content_s) { // -> Result<Value>
Ok(some_value) => Some(some_value),
Err(_) => None
}
}
So I started to re-write the function but became up against the "the type of this value must be known in this context" error:
fn get_json_content_as_btreemap<'a>(content_s : &str) -> Option<&'a BTreeMap<String, Value>> {
match serde_json::from_str(content_s) { // -> Result<Value>
Ok(some) => {
// I expect the type of key_value_pair to be BTreeMap<String, Value>>
// (but I may be wrong!)
let key_value_pair = some.as_object().unwrap(); // Error here
},
Err(_) => None
}
}
I found other questions on stackoverflow like this one:
the type of this value must be known in this context
and using this as a helper, I tried to insert the type as follows:
let key_value_pair = some.as_object::<BTreeMap<_, _>>().unwrap();
which doesnt fix the issue. Also, tried other similar variations to no avail. So how do I fix this please?
EDIT:
I have another function in this app as follows:
fn get_root_value<'a>(json_documemt : &'a Value) -> Result<&'a Value, JsonErrorCode> {
if json_documemt.is_object() {
for (k, v) in json_documemt.as_object().unwrap().iter() {
if k == "root" {
println!("found root: {}", k);
return Ok(v)
}
}
return Err(JsonErrorCode::Custom("Failed to find root node".to_string()))
}
Err(JsonErrorCode::Custom("Not an object".to_string()))
}
... and this works fine. Here you can see that I can call as_object() and then obtain the key and value as a tuple pair. I don't understand why as_object is working in one case but not the other. I would like to pull out the BTreeMap and pass this around as a borrowed item.
You can change the return type of your initial function and serde_json will deserialize to the appropriate object if it can:
fn get_json_content(content_s : &str) -> Option<BTreeMap<String, Value>> {
// instead of returning a value, we need to return a BTreeMap, so we can get the
// key and the value.
println!("===>>> json_content obtained: {}", content_s);
match serde_json::from_str(content_s) { // -> Result<Value>
Ok(some_value) => Some(some_value),
Err(_) => None
}
// Note: this match statement can be rewritten as
// serde_json::from_str(content_s).ok()
}
Your second example won't work because you are instantiating the Value object inside the function, and then trying to return a reference to the object you just instantiated. This won't work because the object will go out of scope at the end of the function and the reference will then be invalid.