Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transform all keys from `underscore` to `camel case` of json objects in circe

Tags:

json

scala

circe

Origin

{
  "first_name" : "foo",
  "last_name" : "bar",
  "parent" : {
    "first_name" : "baz",
    "last_name" : "bazz",
  }
}

Expected

 {
      "firstName" : "foo",
      "lastName" : "bar",
      "parent" : {
        "firstName" : "baz",
        "lastName" : "bazz",
      }
    }

How can I transform all keys of json objects ?

like image 978
jilen Avatar asked Jun 02 '16 06:06

jilen


3 Answers

Here's how I'd write this. It's not as concise as I'd like, but it's not terrible:

import cats.free.Trampoline
import cats.std.list._
import cats.syntax.traverse._
import io.circe.{ Json, JsonObject }

/**
 * Helper method that transforms a single layer.
 */
def transformObjectKeys(obj: JsonObject, f: String => String): JsonObject =
  JsonObject.fromIterable(
    obj.toList.map {
      case (k, v) => f(k) -> v
    }
  )

def transformKeys(json: Json, f: String => String): Trampoline[Json] =
  json.arrayOrObject(
    Trampoline.done(json),
    _.traverse(j => Trampoline.suspend(transformKeys(j, f))).map(Json.fromValues),
    transformObjectKeys(_, f).traverse(obj => Trampoline.suspend(transformKeys(obj, f))).map(Json.fromJsonObject)
  )

And then:

import io.circe.literal._

val doc = json"""
{
  "first_name" : "foo",
  "last_name" : "bar",
  "parent" : {
    "first_name" : "baz",
    "last_name" : "bazz"
  }
}
"""

def sc2cc(in: String) = "_([a-z\\d])".r.replaceAllIn(in, _.group(1).toUpperCase)

And finally:

scala> import cats.std.function._
import cats.std.function._

scala> transformKeys(doc, sc2cc).run
res0: io.circe.Json =
{
  "firstName" : "foo",
  "lastName" : "bar",
  "parent" : {
    "firstName" : "baz",
    "lastName" : "bazz"
  }
}

We probably should have some way of recursively applying a Json => F[Json] transformation like this more conveniently.

like image 63
Travis Brown Avatar answered Nov 17 '22 15:11

Travis Brown


Depending on your full use-case, with the latest Circe you might prefer just leveraging the existing decoder/encoder for converting between camel/snake according to these references:

  • https://dzone.com/articles/5-useful-circe-feature-you-may-have-overlooked
  • https://github.com/circe/circe/issues/663

For instance, in my particular use-case this makes sense because I'm doing other operations that benefit from the type-safety of first deserializing into case classes. So if you're willing to decode the JSON into a case class, and then encode it back into JSON, all you would need is for your (de)serializing code to extend a trait that configures this, like:

import io.circe.derivation._
import io.circe.{Decoder, Encoder, ObjectEncoder, derivation}
import io.circe.generic.auto._
import io.circe.parser.decode
import io.circe.syntax._

trait JsonSnakeParsing {
  implicit val myCustomDecoder: Decoder[MyCaseClass] = deriveDecoder[MyCaseClass](io.circe.derivation.renaming.snakeCase)
  // only needed if you want to serialize back to snake case json:
  // implicit val myCustomEncoder: ObjectEncoder[MyCaseClass] = deriveEncoder[MyCaseClass](io.circe.derivation.renaming.snakeCase)
}

For example, I then extend that when I actually parse or output the JSON:

trait Parsing extends JsonSnakeParsing {

  val result: MyCaseClass = decode[MyCaseClass](scala.io.Source.fromResource("my.json").mkString) match {
    case Left(jsonError) => throw new Exception(jsonError)
    case Right(source) => source
  }

  val theJson = result.asJson
}

For this example, your case class might look like:

case class MyCaseClass(firstName: String, lastName: String, parent: MyCaseClass)

Here's my full list of circe dependencies for this example:

val circeVersion = "0.10.0-M1"

"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"io.circe" %% "circe-generic-extras" % circeVersion,
"io.circe" %% "circe-derivation" % "0.9.0-M5",
like image 25
ecoe Avatar answered Nov 17 '22 17:11

ecoe


def transformKeys(json: Json, f: String => String): TailRec[Json] = {
      if(json.isObject) {
        val obj = json.asObject.get
        val fields = obj.toList.foldLeft(done(List.empty[(String, Json)])) { (r, kv) =>
          val (k, v) = kv
          for {
            fs <- r
            fv <- tailcall(transformKeys(v, f))
          } yield fs :+ (f(k) -> fv)
        }
        fields.map(fs => Json.obj(fs: _*))
      } else if(json.isArray) {
        val arr = json.asArray.get
        val vsRec = arr.foldLeft(done(List.empty[Json])) { (vs, v) =>
          for {
            s <- vs
            e <- tailcall(transformKeys(v, f))
          } yield s :+ e
        }
        vsRec.map(vs => Json.arr(vs: _*))
      } else {
        done(json)
      }
    }

Currently I do transform like this, but is rather complicated, hope there is a simple way.

like image 2
jilen Avatar answered Nov 17 '22 15:11

jilen