Update all values from any Json - json

I've got some json and I have to encrypt all the values. Below is a json, all the values of which should be updated:
json = Json.parse("""{
"key1" : 1.5,
"key2" : [
{"key211": 1, "key212": "value212"},
{"key221": 2, "key222": "value222"}
]
"key3" : {
"key31" : true,
"key32" : "value32"
},
"key4" : 17
}"""
After encrypting and updating all the values, it should look like this:
val json = Json.parse("""{
"key1" : "uhKhbofQtL",
"key2" : [
{"key211": "FxnbGGZFMW", "key212": "VsdfdGfg"},
{"key221": "sdffFdd", "key222": "Fsdfsfds"}
]
"key3" : {
"key31" : "Fsdfasdf",
"key32" : "Vsdfsdfsdfs"
},
"key4" : "sfsdfFSdfs"
}"""
How can I do it?

Parse it as a Map, then traverse the map and encrypt:
def encrypt(data: Any, enc: Any => String): Any = data match {
case v: Map[String, Any] => v.map { case (k,v) => k -> encrypt(v, enc) }
case v: List[Any] => v.map(encrypt(_, enc))
case v => enc(v)
}

I've solved this problem and solution is below
implicit val readsMap: Reads[Map[String, Any]] = Reads[Map[String, Any]](m => Reads.mapReads[Any](metaValueReader).reads(m))
implicit val writesMap: Writes[Map[String, Any]] = Writes[Map[String, Any]](m => Writes.mapWrites[Any](metaValueWriter).writes(m))
def metaValueReader(jsValue: JsValue): JsResult[Any] = jsValue match {
case JsObject(m) => JsSuccess(m.map { case (k, v) => k -> metaValueReader(v) })
case JsArray(arr) => JsSuccess(arr.map(metaValueReader))
case JsBoolean(b) => JsSuccess(b).map(encryptValue)
case JsNumber(n) => JsSuccess(n).map(encryptValue)
case JsString(s) => JsSuccess(s).map(encryptValue)
case JsNull => JsSuccess("").map(encryptValue)
case badValue => JsError(s"$badValue is not a valid value")
}
def metaValueWriter(value: Any): JsValue = value match {
case jsRes: JsSuccess[Any] => metaValueWriter(jsRes.get)
case m: Map[String, Any] => JsObject(m.map { case (k, v) => k -> metaValueWriter(v) })
case arr: Seq[Any] => JsArray(arr.map(metaValueWriter))
case s: String => JsString(s)
}
How can I improve this code?

Related

I am getting error parsing JSON. Why do I get that error?

I have some old code and I forget the right json format.
try
{L} = ejson:decode(JsonStr),
?INFO("json======>~w", [L]),
{_, {L1}} = lists:keyfind(<<"total">>, 1, L),
{_, Id} = lists:keyfind(<<"id">>, 1, L1),
{_, Name} = lists:keyfind(<<"name">>, 1, L1),
{_, IconId} = lists:keyfind(<<"icon_id">>, 1, L1),
{_, Title= lists:keyfind(<<"Title">>, 1, L1),
{_, L2} = lists:keyfind(<<"child">>, 1, L),..
Fun =
fun({L3}) ->
{_, Id1} = lists:keyfind(<<"id">>, 1, L3),
{_, Bid} = lists:keyfind(<<"bid">>, 1, L3),
{_, TotalId1} = lists:keyfind(<<"total_id">>, 1, L3),
...
end,...
So this is my json format:
'total'=>
[
'id' => 1,
'name' => 1,
'icon_id' => 503,
],
'child'=>
[
'id' => 1,
'group' => 0,
'total_id' => 20,
...]
But I am getting error parsing JSON. Why do I get that error?
If this is your actual JSON:
'total'=>
[
'id' => 1,
'name' => 1,
'icon_id' => 503,
],
'child'=>
[
'id' => 1,
'group' => 0,
'total_id' => 20,
...]
then it's malformed. You should check out the JSON spec. The long and the short of it is, JSON objects start and end with curly braces {}, keys are double-quoted "" and the separator between key and value is a colon :.
{
"total" : {
"id" : 1,
"name" : 1,
"icon_id" : 503
},
"child" :
{
"id" : 1,
"group" : 0,
"total_id" : 20
} ...
}
If you're trying to write ejson, that can be expressed as a map:
#{
<<"total">> => #{
<<"id">> => 1,
<<"name">> => 1,
<<"icon_id">> => 503
},
<<"child">> => #{
<<"id">> => 1,
<<"group">> => 0,
<<"total_id">> => 20
}..
}
or a tuple/list thing in erlang terms. There's a really helpful representation in the jiffy README that I've copied here:
Erlang JSON Erlang
==========================================================================
null -> null -> null
true -> true -> true
false -> false -> false
"hi" -> [104, 105] -> [104, 105]
<<"hi">> -> "hi" -> <<"hi">>
hi -> "hi" -> <<"hi">>
1 -> 1 -> 1
1.25 -> 1.25 -> 1.25
[] -> [] -> []
[true, 1.0] -> [true, 1.0] -> [true, 1.0]
{[]} -> {} -> {[]}
{[{foo, bar}]} -> {"foo": "bar"} -> {[{<<"foo">>, <<"bar">>}]}
{[{<<"foo">>, <<"bar">>}]} -> {"foo": "bar"} -> {[{<<"foo">>, <<"bar">>}]}
#{<<"foo">> => <<"bar">>} -> {"foo": "bar"} -> #{<<"foo">> => <<"bar">>}
It looks like your parser is expecting parsing out the ejson into an set of erlang terms like this:
{[{<<"foo">>, <<"bar">>}]} -> {"foo": "bar"} -> {[{<<"foo">>, <<"bar">>}]}
If your decoder has any ability to return a map, I would suggest you send that option along as they are MUCH MUCH easier to work with. If your decoder can't do that, then I would suggest you try and switch to using jiffy.

Parse a nested json structure

I have a nested json file like below
{
"Message No": 1.0,
"abc": {
"action": {
"ab1": false,
"ab2": false
},
"val": "Global"
},
"tyu": {
"lmp": [{
"Currency": "USD",
"Amount": "32401.32"
}]
},
"Payments": {
"Array": ["Hi", "There"],
"Details": [{
"Date": "2019-04-11"
}]
}
}
I have found a piece of code from google which will convert it into a simple key value pair
The code is like below
def Simply(m: Map[String, Any], tree: List[String] = List()) : Iterable[(String, Any)] = m.flatten
{
case (k: String, v: Map[String, Any] #unchecked) => Simply(v, tree :+ k)
case (k: String, v: List[Map[String, Any]] #unchecked) => v.flatten(Simply(_, tree :+ k))
case (k: String, v: Any) => List((tree :+ k.toString).mkString("_") ->v)
case (k,null) => List((tree :+ k.toString).mkString("_") ->"null")
}
The code works fine but it can not process the Array element in the json
"Array": ["Hi", "There"],
I tried to put an extra condition like
case (k: String, v: List[String]) => List((tree :+ k.toString).mkString("_") ->v.mkString(","))
but then this condition is blocking the below case in the simply function
case (k: String, v: List[Map[String, Any]] #unchecked) => v.flatten(Simply(_, tree :+ k))
Please help me understand If I am putting the condition in wrong place or any code change i need to do
Expected output
(tyu_lmp_Amount,32401.32)
(abc_action_ab1,false)
(Message No,1.0)
(abc_action_ab2,false)
(tyu_lmp_Currency,USD)
(Payments_Details_Date,2019-04-11)
(Payments_Array,{Hi, There})
(abc_val,Global)
I hope this is what you want:
package advanced
import org.json4s._
import org.json4s.jackson.JsonMethods._
object JsonTest extends App {
val s =
"""
|{
| "Message No": 1.0,
| "abc": {
| "action": {
| "ab1": false,
| "ab2": false
| },
| "val": "Global"
| },
|
| "tyu": {
| "lmp": [{
| "Currency": "USD",
| "Amount": "32401.32"
| }]
| },
|
| "Payments": {
| "Array": ["Hi", "There"],
| "Details": [{
| "Date": "2019-04-11"
| }]
| }
|}
|""".stripMargin
def jsonStrToMap(jsonStr: String): Map[String, Any] = {
implicit val formats = org.json4s.DefaultFormats
parse(jsonStr).extract[Map[String, Any]]
}
def Simply(m: Map[String, Any], tree: List[String] = List()) : Iterable[(String, Any)] = m.flatMap {
case (k: String, v: Map[String, Any]) => Simply(v, tree :+ k)
case (k: String, v: List[Any]) if v.headOption.exists(_.isInstanceOf[Map[String, Any]]) => v.flatMap{ subNode =>
Simply(subNode.asInstanceOf[Map[String, Any]], tree :+ k)
}
case (k: String, v: List[String]) => List((tree :+ k.toString).mkString("_") -> v.mkString(","))
case (k: String, v: Any) => List((tree :+ k.toString).mkString("_") ->v)
case (k,null) => List((tree :+ k.toString).mkString("_") ->"null")
}
val map = jsonStrToMap(s)
println(Simply(jsonStrToMap(s)))
//
// Map(tyu_lmp_Amount -> 32401.32, abc_action_ab1 -> false, Message No -> 1.0, abc_action_ab2 -> false, tyu_lmp_Currency -> USD, Payments_Details_Date -> 2019-04-11, Payments_Array -> Hi,There, abc_val -> Global)
//
}
pattern match on [Any] will try to cast it to any type in pattern match cases so that v: List[String] is swallowing any subnode that is a list of objects.

How to find the difference/mismatch between two JSON file?

I have two json files, one is expected json and the another one is the result of GET API call. I need to compare and find out the mismatch in the file.
Expected Json:
{
"array": [
1,
2,
3
],
"boolean": true,
"null": null,
"number": 123,
"object": {
"a": "b",
"c": "d",
"e": "f"
},
"string": "Hello World"
}
Actual Json response:
{
"array": [
1,
2,
3
],
"boolean": true,
"null": null,
"number": 456,
"object": {
"a": "b",
"c": "d",
"e": "f"
},
"string": "India"
}
Actually there are two mismatch: number received is 456 and string is India.
Is there a way to compare and get these two mismatch as results.
This need to be implemented in gatling/scala.
You can use, for example, play-json library and recursively traverse both JSONs. For next input (a bit more sophisticated than yours input):
LEFT:
{
"array" : [ 1, 2, 4 ],
"boolean" : true,
"null" : null,
"number" : 123,
"object" : {
"a" : "b",
"c" : "d",
"e" : "f"
},
"string" : "Hello World",
"absent-in-right" : true,
"different-types" : 123
}
RIGHT:
{
"array" : [ 1, 2, 3 ],
"boolean" : true,
"null" : null,
"number" : 456,
"object" : {
"a" : "b",
"c" : "d",
"e" : "ff"
},
"string" : "India",
"absent-in-left" : true,
"different-types" : "YES"
}
It produces this output:
Next fields are absent in LEFT:
*\absent-in-left
Next fields are absent in RIGHT:
*\absent-in-right
'*\array\(2)' => 4 != 3
'*\number' => 123 != 456
'*\object\e' => f != ff
'*\string' => Hello World != India
Cannot compare JsNumber and JsString in '*\different-types'
Code:
val left = Json.parse("""{"array":[1,2,4],"boolean":true,"null":null,"number":123,"object":{"a":"b","c":"d","e":"f"},"string":"Hello World","absent-in-right":true,"different-types":123}""").asInstanceOf[JsObject]
val right = Json.parse("""{"array":[1,2,3],"boolean":true,"null":null,"number":456,"object":{"a":"b","c":"d","e":"ff"},"string":"India","absent-in-left":true,"different-types":"YES"}""").asInstanceOf[JsObject]
// '*' - for the root node
showJsDiff(left, right, "*", Seq.empty[String])
def showJsDiff(left: JsValue, right: JsValue, parent: String, path: Seq[String]): Unit = {
val newPath = path :+ parent
if (left.getClass != right.getClass) {
println(s"Cannot compare ${left.getClass.getSimpleName} and ${right.getClass.getSimpleName} " +
s"in '${getPath(newPath)}'")
}
else {
left match {
// Primitive types are pretty easy to handle
case JsNull => logIfNotEqual(JsNull, right.asInstanceOf[JsNull.type], newPath)
case JsBoolean(value) => logIfNotEqual(value, right.asInstanceOf[JsBoolean].value, newPath)
case JsNumber(value) => logIfNotEqual(value, right.asInstanceOf[JsNumber].value, newPath)
case JsString(value) => logIfNotEqual(value, right.asInstanceOf[JsString].value, newPath)
case JsArray(value) =>
// For array we have to call showJsDiff on each element of array
val arr1 = value
val arr2 = right.asInstanceOf[JsArray].value
if (arr1.length != arr2.length) {
println(s"Arrays in '${getPath(newPath)}' have different length. ${arr1.length} != ${arr2.length}")
}
else {
arr1.indices.foreach { idx =>
showJsDiff(arr1(idx), arr2(idx), s"($idx)", newPath)
}
}
case JsObject(value) =>
val leftFields = value.keys.toSeq
val rightJsObject = right.asInstanceOf[JsObject]
val rightFields = rightJsObject.fields.map { case (name, value) => name }
val absentInLeft = rightFields.diff(leftFields)
if (absentInLeft.nonEmpty) {
println("Next fields are absent in LEFT: ")
absentInLeft.foreach { fieldName =>
println(s"\t ${getPath(newPath :+ fieldName)}")
}
}
val absentInRight = leftFields.diff(rightFields)
if (absentInRight.nonEmpty) {
println("Next fields are absent in RIGHT: ")
absentInRight.foreach { fieldName =>
println(s"\t ${getPath(newPath :+ fieldName)}")
}
}
// For common fields we have to call showJsDiff on them
val commonFields = leftFields.intersect(rightFields)
commonFields.foreach { field =>
showJsDiff(value(field), rightJsObject(field), field, newPath)
}
}
}
}
def logIfNotEqual[T](left: T, right: T, path: Seq[String]): Unit = {
if (left != right) {
println(s"'${getPath(path)}' => $left != $right")
}
}
def getPath(path: Seq[String]): String = path.mkString("\\")
Use diffson - a Scala implementation of RFC-6901 and RFC-6902: https://github.com/gnieh/diffson
json4s has a handy diff function described here: https://github.com/json4s/json4s (search for Merging & Diffing) and API doc here: https://static.javadoc.io/org.json4s/json4s-core_2.9.1/3.0.0/org/json4s/Diff.html
This is a slightly modified version of Artavazd's answer (which is amazing btw thank you so much!). This version outputs the differences into a convenient object instead of only logging them.
import play.api.Logger
import play.api.libs.json.{JsArray, JsBoolean, JsError, JsNull, JsNumber, JsObject, JsString, JsSuccess, JsValue, Json, OFormat, Reads}
case class JsDifferences(
differences: List[JsDifference] = List()
)
object JsDifferences {
implicit val format: OFormat[JsDifferences] = Json.format[JsDifferences]
}
case class JsDifference(
key: String,
path: Seq[String],
oldValue: Option[String],
newValue: Option[String]
)
object JsDifference {
implicit val format: OFormat[JsDifference] = Json.format[JsDifference]
}
object JsonUtils {
val logger: Logger = Logger(this.getClass)
def findDiff(left: JsValue, right: JsValue, parent: String = "*", path: List[String] = List()): JsDifferences = {
val newPath = path :+ parent
if (left.getClass != right.getClass) {
logger.debug(s"Cannot compare ${left.getClass.getSimpleName} and ${right.getClass.getSimpleName} in '${getPath(newPath)}'")
JsDifferences()
} else left match {
case JsNull => logIfNotEqual(JsNull, right.asInstanceOf[JsNull.type], newPath)
case JsBoolean(value) => logIfNotEqual(value, right.asInstanceOf[JsBoolean].value, newPath)
case JsNumber(value) => logIfNotEqual(value, right.asInstanceOf[JsNumber].value, newPath)
case JsString(value) => logIfNotEqual(value, right.asInstanceOf[JsString].value, newPath)
case JsArray(value) =>
val arr1 = value
val arr2 = right.asInstanceOf[JsArray].value
if (arr1.length != arr2.length) {
logger.debug(s"Arrays in '${getPath(newPath)}' have different length. ${arr1.length} != ${arr2.length}")
JsDifferences()
} else JsDifferences(arr1.indices.flatMap(idx => findDiff(arr1(idx), arr2(idx), s"($idx)", newPath).differences).toList)
case leftJsObject: JsObject => {
val leftFields = leftJsObject.keys.toSeq
val rightJsObject = right.asInstanceOf[JsObject]
val rightFields = rightJsObject.fields.map { case (name, value) => name }
val keysAbsentInLeft = rightFields.diff(leftFields)
val leftDifferences = keysAbsentInLeft.map(fieldName => JsDifference(
key = fieldName, path = newPath :+ fieldName, oldValue = None, newValue = Some(rightJsObject(fieldName).toString)
))
val keysAbsentInRight = leftFields.diff(rightFields)
val rightDifferences = keysAbsentInRight.map(fieldName => JsDifference(
key = fieldName, path = newPath :+ fieldName, oldValue = Some(leftJsObject(fieldName).toString), newValue = None
))
val commonKeys = leftFields.intersect(rightFields)
val commonDifferences = commonKeys.flatMap(field => findDiff(leftJsObject(field), rightJsObject(field), field, newPath).differences).toList
JsDifferences((leftDifferences ++ rightDifferences ++ commonDifferences).toList)
}
}
}
def logIfNotEqual[T](left: T, right: T, path: Seq[String]): JsDifferences = {
if (left != right) {
JsDifferences(List(JsDifference(
key = path.last, path = path, oldValue = Some(left.toString), newValue = Some(right.toString)
)))
} else JsDifferences()
}
def getPath(path: Seq[String]): String = path.mkString("\\")
}

How to convert Scala JsArray to custorm object

I'm new to Scala and don't see a way to do this.
I have this class:
case class User(userId: Int, userName: String, email: String, password:
String) {
def this() = this(0, "", "", "")
}
case class Team(teamId: Int, teamName: String, teamOwner: Int,
teamMembers: List[User]) {
def this() = this(0, "", 0, Nil)
}
I'm sending post request as :-
'{
"teamId" : 9,
"teamName" : "team name",
"teamOwner" : 2,
"teamMembers" : [ {
"userId" : 1000,
"userName" : "I am new member",
"email" : "eamil",
"password" : "password"
}]
}'
I get request:-
I tried:-
val data = (request.body \ "teamMembers")
val data2 = (request.body \ "teamId")
val data3 = (request.body \ "teamName")
data: [{"userId":1000,"userName":"I am new
member","email":"eamil","password":"password"}]
data2: 9
data3: "team name"
How to convert data to User object?
[{"userId":1000,"userName":"I am new
member","email":"email","password":"password"}]
As an option, you can read Users like this
import play.api.libs.json.{JsArray, Json}
case class User(
userId: Int,
userName: String,
email: String,
password: String) {
}
case class Team(
teamId: Int,
teamName: String,
teamOwner: Int,
teamMembers: List[User]) {
}
implicit val userFormat = Json.format[User]
implicit val teamFormat = Json.format[Team]
val jsonStr = """{
"teamId" : 9,
"teamName" : "team name",
"teamOwner" : 2,
"teamMembers" : [ {
"userId" : 1000,
"userName" : "I am new member",
"email" : "eamil",
"password" : "password"
}]
}"""
val json = Json.parse(jsonStr)
// Team(9,team name,2,List(User(1000,I am new member,eamil,password)))
json.as[Team]
// Seq[User] = ListBuffer(User(1000,I am new member,eamil,password))
val users = (json \ "teamMembers").as[JsArray].value.map(_.as[User])

Play [Scala]: How to flatten a JSON object

Given the following JSON...
{
"metadata": {
"id": "1234",
"type": "file",
"length": 395
}
}
... how do I convert it to
{
"metadata.id": "1234",
"metadata.type": "file",
"metadata.length": 395
}
Tx.
You can do this pretty concisely with Play's JSON transformers. The following is off the top of my head, and I'm sure it could be greatly improved on:
import play.api.libs.json._
val flattenMeta = (__ \ 'metadata).read[JsObject].flatMap(
_.fields.foldLeft((__ \ 'metadata).json.prune) {
case (acc, (k, v)) => acc andThen __.json.update(
Reads.of[JsObject].map(_ + (s"metadata.$k" -> v))
)
}
)
And then:
val json = Json.parse("""
{
"metadata": {
"id": "1234",
"type": "file",
"length": 395
}
}
""")
And:
scala> json.transform(flattenMeta).foreach(Json.prettyPrint _ andThen println)
{
"metadata.id" : "1234",
"metadata.type" : "file",
"metadata.length" : 395
}
Just change the path if you want to handle metadata fields somewhere else in the tree.
Note that using a transformer may be overkill hereā€”see e.g. Pascal Voitot's input in this thread, where he proposes the following:
(json \ "metadata").as[JsObject].fields.foldLeft(Json.obj()) {
case (acc, (k, v)) => acc + (s"metadata.$k" -> v)
}
It's not as composable, and you'd probably not want to use as in real code, but it may be all you need.
This is definitely not trivial, but possible by trying to flatten it recursively. I haven't tested this thoroughly, but it works with your example and some other basic one's I've come up with using arrays:
object JsFlattener {
def apply(js: JsValue): JsValue = flatten(js).foldLeft(JsObject(Nil))(_++_.as[JsObject])
def flatten(js: JsValue, prefix: String = ""): Seq[JsValue] = {
js.as[JsObject].fieldSet.toSeq.flatMap{ case (key, values) =>
values match {
case JsBoolean(x) => Seq(Json.obj(concat(prefix, key) -> x))
case JsNumber(x) => Seq(Json.obj(concat(prefix, key) -> x))
case JsString(x) => Seq(Json.obj(concat(prefix, key) -> x))
case JsArray(seq) => seq.zipWithIndex.flatMap{ case (x, i) => flatten(x, concat(prefix, key + s"[$i]")) }
case x: JsObject => flatten(x, concat(prefix, key))
case _ => Seq(Json.obj(concat(prefix, key) -> JsNull))
}
}
}
def concat(prefix: String, key: String): String = if(prefix.nonEmpty) s"$prefix.$key" else key
}
JsObject has the fieldSet method that returns a Set[(String, JsValue)], which I mapped, matched against the JsValue subclass, and continued consuming recursively from there.
You can use this example by passing a JsValue to apply:
val json = Json.parse("""
{
"metadata": {
"id": "1234",
"type": "file",
"length": 395
}
}
"""
JsFlattener(json)
We'll leave it as an exercise to the reader to make the code more beautiful looking.
Here's my take on this problem, based on #Travis Brown's 2nd solution.
It recursively traverses the json and prefixes each key with its parent's key.
def flatten(js: JsValue, prefix: String = ""): JsObject = js.as[JsObject].fields.foldLeft(Json.obj()) {
case (acc, (k, v: JsObject)) => {
if(prefix.isEmpty) acc.deepMerge(flatten(v, k))
else acc.deepMerge(flatten(v, s"$prefix.$k"))
}
case (acc, (k, v)) => {
if(prefix.isEmpty) acc + (k -> v)
else acc + (s"$prefix.$k" -> v)
}
}
which turns this:
{
"metadata": {
"id": "1234",
"type": "file",
"length": 395
},
"foo": "bar",
"person": {
"first": "peter",
"last": "smith",
"address": {
"city": "Ottawa",
"country": "Canada"
}
}
}
into this:
{
"metadata.id": "1234",
"metadata.type": "file",
"metadata.length": 395,
"foo": "bar",
"person.first": "peter",
"person.last": "smith",
"person.address.city": "Ottawa",
"person.address.country": "Canada"
}
#Trev has the best solution here, completely generic and recursive, but it's missing a case for array support. I'd like something that works in this scenario:
turn this:
{
"metadata": {
"id": "1234",
"type": "file",
"length": 395
},
"foo": "bar",
"person": {
"first": "peter",
"last": "smith",
"address": {
"city": "Ottawa",
"country": "Canada"
},
"kids": ["Bob", "Sam"]
}
}
into this:
{
"metadata.id": "1234",
"metadata.type": "file",
"metadata.length": 395,
"foo": "bar",
"person.first": "peter",
"person.last": "smith",
"person.address.city": "Ottawa",
"person.address.country": "Canada",
"person.kids[0]": "Bob",
"person.kids[1]": "Sam"
}
I've arrived at this, which appears to work, but seems overly verbose. Any help in making this pretty would be appreciated.
def flatten(js: JsValue, prefix: String = ""): JsObject = js.as[JsObject].fields.foldLeft(Json.obj()) {
case (acc, (k, v: JsObject)) => {
val nk = if(prefix.isEmpty) k else s"$prefix.$k"
acc.deepMerge(flatten(v, nk))
}
case (acc, (k, v: JsArray)) => {
val nk = if(prefix.isEmpty) k else s"$prefix.$k"
val arr = flattenArray(v, nk).foldLeft(Json.obj())(_++_)
acc.deepMerge(arr)
}
case (acc, (k, v)) => {
val nk = if(prefix.isEmpty) k else s"$prefix.$k"
acc + (nk -> v)
}
}
def flattenArray(a: JsArray, k: String = ""): Seq[JsObject] = {
flattenSeq(a.value.zipWithIndex.map {
case (o: JsObject, i: Int) =>
flatten(o, s"$k[$i]")
case (o: JsArray, i: Int) =>
flattenArray(o, s"$k[$i]")
case a =>
Json.obj(s"$k[${a._2}]" -> a._1)
})
}
def flattenSeq(s: Seq[Any], b: Seq[JsObject] = Seq()): Seq[JsObject] = {
s.foldLeft[Seq[JsObject]](b){
case (acc, v: JsObject) =>
acc:+v
case (acc, v: Seq[Any]) =>
flattenSeq(v, acc)
}
}
Thanks m-z, it is very helpful. (I'm not so familiar with Scala.)
I'd like to add a line for "flatten" working with primitive JSON array like "{metadata: ["aaa", "bob"]}".
def flatten(js: JsValue, prefix: String = ""): Seq[JsValue] = {
// JSON primitive array can't convert to JsObject
if(!js.isInstanceOf[JsObject]) return Seq(Json.obj(prefix -> js))
js.as[JsObject].fieldSet.toSeq.flatMap{ case (key, values) =>
values match {
case JsBoolean(x) => Seq(Json.obj(concat(prefix, key) -> x))
case JsNumber(x) => Seq(Json.obj(concat(prefix, key) -> x))
case JsString(x) => Seq(Json.obj(concat(prefix, key) -> x))
case JsArray(seq) => seq.zipWithIndex.flatMap{ case (x, i) => flatten(x, concat(prefix, key + s"[$i]")) }
case x: JsObject => flatten(x, concat(prefix, key))
case _ => Seq(Json.obj(concat(prefix, key) -> JsNull))
}
}
}
Based on previous solutions, have tried to simplify the code a bit
def getNewKey(oldKey: String, newKey: String): String = {
if (oldKey.nonEmpty) oldKey + "." + newKey else newKey
}
def flatten(js: JsValue, prefix: String = ""): JsObject = {
if (!js.isInstanceOf[JsObject]) return Json.obj(prefix -> js)
js.as[JsObject].fields.foldLeft(Json.obj()) {
case (o, (k, value)) => {
o.deepMerge(value match {
case x: JsArray => x.as[Seq[JsValue]].zipWithIndex.foldLeft(o) {
case (o, (n, i: Int)) => o.deepMerge(
flatten(n.as[JsValue], getNewKey(prefix, k) + s"[$i]")
)
}
case x: JsObject => flatten(x, getNewKey(prefix, k))
case x => Json.obj(getNewKey(prefix, k) -> x.as[JsValue])
})
}
}
}