Search code examples
scalacirce

Replace JSON fields with Scala Circe


Is there a proper way of changing all the values of the fields with a given key using Scala Circe?

The scenario

I have n fields named validTo as dates (e.g. "2024-09-12T00:00:00.000+00:00") and I need to shift those dates by a given number of days, so I need a way to find and replace all those validTo fields in that JSON.

IMPORTANT NOTE: I don't know the JsonPath of those fields in advance, but only their key (name).

I see that there is a function, inspired by Play framework, to find all the values of a given key in Circe: io.circe.Json#findAllByKey:

  /**
   * Recursively return all values matching the specified `key`.
   *
   * The Play docs, from which this method was inspired, reads:
   *   "Lookup for fieldName in the current object and all descendants."
   */
  final def findAllByKey(key: String): List[Json]

Solution

  • I will use something like this:

    //> using dep io.circe::circe-parser::0.14.6
    //> using dep org.typelevel::cats-core::2.10.0
    
    import scala.util.*
    import io.circe.*, io.circe.parser.*
    import java.time.LocalDate
    import java.time.format.DateTimeFormatter
    
    val rawJson: String = """
    {
      "foo": "bar",
      "validTo": "2020-10-11",
      "list of stuff": {
        "validTo": "2020-10-10",
        "pippo": 2
      }
    }
    """
    
    val formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE
    
    def parseDate(s: String): String = Try(LocalDate.parse(s, formatter)) match
      case Success(date) => formatter.format(date.plusDays(10))
      case Failure(_)    => s
    
    def alter(json: Json): Json = json
      .mapArray(_.map(alter))
      .mapObject(obj =>
        JsonObject.fromIterable(obj.toList.map {
          case ("validTo", x) => ("validTo", x.mapString(parseDate))
          case (x, j)         => (x, j.foldWith(this))
        })
      )
    
    object circe extends App:
      val json = parse(rawJson).getOrElse(Json.Null)
      println(alter(json).spaces2SortKeys)
    
    

    where in the parseDate function you can handle as you want the date case, maybe even throwing exception

    Alternatively, you can follow what @Daenyth said and use a Folder[json]:

    val folder = new Json.Folder[Json] {
      def onNull: Json = Json.Null
      def onBoolean(value: Boolean): Json = Json.fromBoolean(value)
      def onNumber(value: JsonNumber): Json = Json.fromJsonNumber(value)
      def onString(value: String): Json = Json.fromString(value)
      def onArray(value: Vector[Json]): Json =
        Json.fromValues(value.map(_.foldWith(this)))
      def onObject(value: JsonObject): Json =
        Json.fromJsonObject(
          JsonObject.fromIterable(value.toList.map {
            case ("validTo", x) => ("validTo", x.mapString(parseDate))
            case (x, j)         => (x, alter(j))
          })
        )
      }
    
    def alterFold(json:Json) = json.foldWith(folder)