'How to modify values of JsonObject

I want to add a new field to jsonObject and this new field's name will be based on a value of another field. To be clear, this an examples of what I want to achieve.

{
  "values": [
    {
      "id": "1",
      "properties": [
        {
          "stat": "memory",
          "data": 8
        },
        {
          "stat": "cpu",
          "data": 4
        }
      ]
    },
    {
      "id": "2",
      "properties": [
        {
          "stat": "status",
          "data": "OK"
        },
        {
          "stat": "cpu",
          "data": 4
        }
      ]
    }
  ]
}

I want to add a new field to each json object that will have the value of field "stat" as name.

{
  "values": [
    {
      "id": "1",
      "properties": [
        {
          "stat": "memory",
          "data": 8,
          "memory": 8
        },
        {
          "stat": "cpu",
          "data": 4,
          "cpu": 4
        }
      ]
    },
    {
      "id": "2",
      "properties": [
        {
          "stat": "status",
          "data": 0,
          "status": 0
        },
        {
          "stat": "cpu",
          "data": 4,
          "cpu": 4
        }
      ]
    }
  ]
}

I have tried to do the following with JsonPath library but for me it's an ugly solution as I will parse the json three times and I do some manual replacements.

val configuration = Configuration.builder().options(Option.DEFAULT_PATH_LEAF_TO_NULL, Option.ALWAYS_RETURN_LIST).build()
val jsonContext5 = JsonPath.using(configuration).parse(jsonStr)
val listData = jsonContext.read("$['values'][*]['properties'][*]['data']").toString
      .replace("[", "").replace("]", "").split(",").toList
val listStat = jsonContext.read("$['values'][*]['properties'][*]['stat']").toString
      .replace("[", "").replace("]", "")
      .replace("\"", "").split(",").toList
// Replacing values of "stat" by values of "data"
jsonContext5.map("$['values'][*]['properties'][*]['stat']", new MapFunction() {
      var count = - 1
      override def map(currentValue: Any, configuration: Configuration): AnyRef = {
        count += 1
        listData(count)
      }
    })
// replace field stat by its value
for( count <- 0 to listStat.size - 1){
     val path = s"['values'][*]['properties'][$count]"
     jsonContext5.renameKey(path, "stat", s"${listStat(count)}")
}
    

This is the result obtained

{
  "values": [
    {
      "id": "1",
      "properties": [
        {
          "data": 8,
          "memory": "8"
        },
        {
          "data": 4,
          "cpu": "4"
        }
      ]
    },
    {
      "id": "2",
      "properties": [
        {
          "data": 0,
          "memory": "0"
        },
        {
          "data": 4,
          "cpu": "4"
        }
      ]
    }
  ]
}

Is there any better method to achieve this result ? I tried to do it with gson but it's not good handling paths.

This a way to do it with Gson but I will lose the information about other columns since I'm creating another json.

val jsonArray = jsonObject.get("properties").getAsJsonArray
val iter = jsonArray.iterator()
val agreedJson = new JsonArray()
while(iter.hasNext) {
    val json = iter.next().getAsJsonObject
    agreedJson.add(replaceCols(json))
}
def replaceCols(json: JsonObject) = {
    val fieldName = "stat"
    if(json.has(fieldName)) {
      val columnName = json.get(fieldName).getAsString
      val value: String = if (json.has("data")) json.get("data").getAsString else ""
      json.addProperty(columnName, value)
    }
    json
}


Solution 1:[1]

How about something like this?

private static void statDup(final JSONObject o) {
    if (o.containsKey("properties")) {
        final JSONArray a = (JSONArray) o.get("properties");
        for (final Object e : a) {
            final JSONObject p = (JSONObject) e;
            p.put(p.get("stat"), p.get("data"));
        }
    } else {
        for (final Object key : o.keySet()) {
            final Object value = o.get(key);
            if (value instanceof JSONArray) {
                for (final Object e : (JSONArray) value) {
                    statDup((JSONObject) e);
                }
            }
        }
    }
}

Solution 2:[2]

Using Gson, what you should do is create a base class that represents your initial JSON object. Then, extend that class and add the additional attribute(s) you want to add, such as "stat". Then, load the JSON objects into memory, either one by one or all together, then make the necessary changes to each to encompass your changes. Then, map those changes to the new class if you didn't in the prior step, and serialize them to a file or some other storage.

Solution 3:[3]

This is type-safe, a pure FP circe implementation with circe-optics:

object CirceOptics extends App {
  import cats.Applicative
  import cats.implicits._
  import io.circe.{Error => _, _}
  import io.circe.syntax._
  import io.circe.parser._
  import io.circe.optics.JsonPath._

  val jsonStr: String = ???

  def getStat(json: Json): Either[Error, String] =
    root.stat.string.getOption(json)
      .toRight(new Error(s"Missing stat of string type in $json"))

  def getData(json: Json): Either[Error, Json] =
    root.data.json.getOption(json)
      .toRight(new Error(s"Missing data of json type in $json"))
  
  def setField(json: Json, key: String, value: Json) =
    root.at(key).setOption(Some(value))(json)
      .toRight(new Error(s"Unable to set $key -> $value to $json"))
      
  def modifyAllPropertiesOfAllValuesWith[F[_]: Applicative](f: Json => F[Json])(json: Json): F[Json] =
    root.values.each.properties.each.json.modifyF(f)(json)

  val res = for {
    json          <-  parse(jsonStr)
    modifiedJson  <-  modifyAllPropertiesOfAllValuesWith { j =>
      for {
        stat  <-  getStat(j)
        data  <-  getData(j)
        prop  <-  setField(j, stat, data)
      } yield prop
    } (json)
  } yield modifiedJson

  println(res)
}

Solution 4:[4]

The previous answer from Gene McCulley gives a solution with Java and using class net.minidev.json. This answer is using class Gson and written in Scala.

def statDup(o: JsonObject): JsonObject = {
    if (o.has("properties")) {
      val a = o.get("properties").getAsJsonArray
      a.foreach { e =>
        val p = e.getAsJsonObject
        p.add(p.get("stat").getAsString, p.get("data"))
      }
    } else {
      o.keySet.foreach { key =>
        o.get(key) match {
          case jsonArr: JsonArray =>
            jsonArr.foreach { e =>
              statDup(e.getAsJsonObject)
            }
        }
      }
    }
    o
  }

Solution 5:[5]

Your task is to add a new field to each record under each properties in the JSON file, make the current stat value the field name and data values the new field values. The code will be rather long if you try to do it in Java.

Suggest you using SPL, an open-source Java package to get it done. Coding will be very easy and you only need one line:

A
1 =json(json(file("data.json").read()).values.
run(properties=properties.(([["stat","data"]|stat]|[~.array()|data]).record())))

SPL offers JDBC driver to be invoked by Java. Just store the above SPL script as addfield.splx and invoke it in a Java application as you call a stored procedure:

…
Class.forName("com.esproc.jdbc.InternalDriver");
con= DriverManager.getConnection("jdbc:esproc:local://");
st = con.prepareCall("call addfield()");
st.execute();
…

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Gene McCulley
Solution 2 Jason
Solution 3
Solution 4
Solution 5 LeoTaylor