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:
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",