Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transforming JSON with state in circe

Tags:

json

scala

circe

Note: I'm copying this question over from the circe Gitter channel for the sake of posterity.

Suppose we want to translate this JSON document:

{
  "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"
  }]
}

Into this:

{
  "places": [{
    "id": "1"
  }, {
    "id": "2"
  }],
  "transitions": [{
    "id": "3",
    "startPlaceId": "ref:1",
    "endPlaceId": "ref:2"
  }],
  "routes": [{
    "id": "4",
    "transitionId": "ref:3"
  }]
}

I.e., we want to replace the UUID in every id with a simple incremented numeric identifier, and to replace all references to each UUID with references to the new identifiers.

How can we do this with circe?

like image 735
Travis Brown Avatar asked Apr 13 '16 17:04

Travis Brown


1 Answers

It's possible to accomplish this relatively straightforwardly with the state monad transformer from Cats (a library that circe depends on):

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 isn't a legitimate UUID, or if any other field contains a reference to an unknown UUID, the computation will fail with None. This behavior could be refined a bit as needed, and if you wanted more specific information about where the error occurred, you could adapt the implementation to work with cursors instead of JSON values (but this would get a little more complex).

like image 59
Travis Brown Avatar answered Nov 06 '22 20:11

Travis Brown