sentenza
sentenza

Reputation: 1730

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]

Upvotes: 1

Views: 313

Answers (2)

Tonio Gela
Tonio Gela

Reputation: 120

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)

Upvotes: 2

Daenyth
Daenyth

Reputation: 37431

You can do recursive transformations on circe json by using the fold or foldWith methods

Pass a Folder[Json] (or the equivalent functions). For each type other than json objects, return your input as the relevant json type. For objects, transform their keys, probably using JsonObject#toMap

Upvotes: 2

Related Questions