Search code examples
jsonscalacirce

How to insert an empty object into JSON using Circe?


I'm getting a JSON object over the network, as a String. I'm then using Circe to parse it. I want to add a handful of fields to it, and then pass it on downstream.

Almost all of that works.

The problem is that my "adding" is really "overwriting". That's actually ok, as long as I add an empty object first. How can I add such an empty object?

So looking at the code below, I am overwriting "sometimes_empty:{}" and it works. But because sometimes_empty is not always empty, it results in some data loss. I'd like to add a field like: "custom:{}" and then ovewrite the value of custom with my existing code.

Two StackOverflow posts were helpful. One worked, but wasn't quite what I was looking for. The other I couldn't get to work.

1: Modifying a JSON array in Scala with circe

2: Adding field to a json using Circe

val js: String = """
{
  "id": "19",
  "type": "Party",
  "field": {
    "id": 1482,
    "name": "Anne Party",
    "url": "https"
  },
  "sometimes_empty": {

  },
  "bool": true,
  "timestamp": "2018-12-18T11:39:18Z"
}
"""

val newJson = parse(js).toOption
  .flatMap { doc =>
    doc.hcursor
      .downField("sometimes_empty")
      .withFocus(_ =>
        Json.fromFields(
          Seq(
            ("myUrl", Json.fromString(myUrl)),
            ("valueZ", Json.fromString(valueZ)),
            ("valueQ", Json.fromString(valueQ)),
            ("balloons", Json.fromString(balloons))
          )
        )
      )
      .top
  }

newJson match {
  case Some(v) => return v.toString
  case None => println("Failure!")
}

Solution

  • We need to do a couple of things. First, we need to zoom in on the specific property we want to update, if it doesn't exist, we'll create a new empty one. Then, we turn the zoomed in property in the form of a Json into JsonObject in order to be able to modify it using the +: method. Once we've done that, we need to take the updated property and re-introduce it in the original parsed JSON to get the complete result:

    import io.circe.{Json, JsonObject, parser}
    import io.circe.syntax._
    
    object JsonTest {
      def main(args: Array[String]): Unit = {
        val js: String =
          """
            |{
            |  "id": "19",
            |  "type": "Party",
            |  "field": {
            |    "id": 1482,
            |    "name": "Anne Party",
            |    "url": "https"
            |  },
            |  "bool": true,
            |  "timestamp": "2018-12-18T11:39:18Z"
            |}
          """.stripMargin
    
        val maybeAppendedJson =
          for {
            json <- parser.parse(js).toOption
            sometimesEmpty <- json.hcursor
              .downField("sometimes_empty")
              .focus
              .orElse(Option(Json.fromJsonObject(JsonObject.empty)))
            jsonObject <- json.asObject
            emptyFieldJson <- sometimesEmpty.asObject
            appendedField = emptyFieldJson.+:("added", Json.fromBoolean(true))
            res = jsonObject.+:("sometimes_empty", appendedField.asJson)
          } yield res
    
        maybeAppendedJson.foreach(obj => println(obj.asJson.spaces2))
      }
    }
    

    Yields:

    {
      "id" : "19",
      "type" : "Party",
      "field" : {
        "id" : 1482,
        "name" : "Anne Party",
        "url" : "https"
      },
      "sometimes_empty" : {
        "added" : true,
        "someProperty" : true
      },
      "bool" : true,
      "timestamp" : "2018-12-18T11:39:18Z"
    }