This can be done relatively directly with the state monad transformer from Cats (from the library, which depends on the cycle):
import cats.data.StateT import cats.std.option._ import cats.std.list._ import cats.syntax.traverse._ import io.circe.{ Json, JsonObject } import java.util.UUID def update(j: Json): StateT[Option, Map[UUID, Long], Json] = j.arrayOrObject( StateT.pure[Option, Map[UUID, Long], Json](j), _.traverseU(update).map(Json.fromValues), _.toList.traverseU { case ("id", value) => StateT { (ids: Map[UUID, Long]) => value.as[UUID].toOption.map { uuid => val next = if (ids.isEmpty) 1L else ids.values.max + 1L (ids.updated(uuid, next), "id" -> Json.fromString(s"$next")) } } case (other, value) => value.as[UUID].toOption match { case Some(uuid) => StateT { (ids: Map[UUID, Long]) => ids.get(uuid).map(id => (ids, other -> Json.fromString(s"ref:$id"))) } case None => update(value).map(other -> _) } }.map(Json.fromFields) )
And then:
import io.circe.literal._ val doc: Json = json""" { "places": [{ "id": "dadcc0d9-0615-4e46-9df4-2619f49930a0" }, { "id": "21d02f4b-7e88-47d7-bf2b-48e50761b6c3" }], "transitions": [{ "id": "10a3aee5-541b-4d04-bb45-cb1dbbfe2128", "startPlaceId": "dadcc0d9-0615-4e46-9df4-2619f49930a0", "endPlaceId": "21d02f4b-7e88-47d7-bf2b-48e50761b6c3" }], "routes": [{ "id": "6ded1763-86c0-44ce-b94b-f05934976a3b", "transitionId": "10a3aee5-541b-4d04-bb45-cb1dbbfe2128" }] } """
And finally:
scala> import cats.std.long._ import cats.std.long._ scala> import cats.std.map._ import cats.std.map._ scala> update(doc).runEmptyA res0: Option[io.circe.Json] = Some({ "places" : [ { "id" : "1" }, { "id" : "2" } ], "transitions" : [ { "id" : "3", "startPlaceId" : "ref:1", "endPlaceId" : "ref:2" } ], "routes" : [ { "id" : "4", "transitionId" : "ref:3" } ] })
If any id field is not a legal UUID, or if any other field contains a link to an unknown UUID, the calculation will end with None . This behavior could be clarified as necessary, and if you need more specific information about where the error occurred, you can adapt the implementation to work with cursors instead of JSON values ββ(but this has become a bit more complicated).