Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Circe parse json from snake case keys

Tags:

scala

circe

I have the following case class:

final case class Camel(firstName: String, lastName: String, waterPerDay: Int)

and circe configuration:

object CirceImplicits {

  import io.circe.syntax._
  import io.circe.generic.semiauto._
  import io.circe.{Encoder, Decoder, Json}
  import io.circe.generic.extras.Configuration

  implicit val customConfig: Configuration =
    Configuration.default.withSnakeCaseMemberNames.withDefaults
  implicit lazy val camelEncoder: Encoder[Camel] = deriveEncoder
  implicit lazy val camelDecoder: Decoder[Camel] = deriveDecoder
}

It is ok, when testing against this:

val camel = Camel(firstName = "Camelbek", lastName = "Camelov", waterPerDay = 30)

private val camelJ = Json.obj(
    "firstName" -> Json.fromString("Camelbek"),
    "lastName" -> Json.fromString("Camelov"),
    "waterPerDay" -> Json.fromInt(30)
)

"Decoder" must "decode camel types" in {
    camelJ.as[Camel] shouldBe Right(camel)
}

But this test is not passing:

val camel = Camel(firstName = "Camelbek", lastName = "Camelov", waterPerDay = 30)

private val camelJ = Json.obj(
    "first_name" -> Json.fromString("Camelbek"),
    "last_name" -> Json.fromString("Camelov"),
    "water_per_day" -> Json.fromInt(30)
)

"Decoder" must "decode camel types" in {
    camelJ.as[Camel] shouldBe Right(camel)
}

How correctly configure circe in order to be able parsing json with keys in snake case?

I'm using circe version 0.10.0

like image 862
oybek Avatar asked Apr 26 '19 11:04

oybek


1 Answers

Solution 1

Circe gets field names from your case class instance and traverses through JSON using cursor, tries to get the value of each field name and tries to convert it to your desirable type.

It means that your decoder won't be able to process both cases.

The solution to this problem is to write two decoders:

  1. Basic decoder (deriveEncoder will work)
  2. Encoder which uses HCursor to navigate through your JSON and get snake case keys
val decoderDerived: Decoder[Camel] = deriveDecoder
val decoderCamelSnake: Decoder[Camel] = (c: HCursor) =>
    for {
      firstName <- c.downField("first_name").as[String]
      lastName <- c.downField("last_name").as[String]
      waterPerDay <- c.downField("water_per_day").as[Int]
    } yield {
      Camel(firstName, lastName, waterPerDay)
    }

Then you can combine these two decoders into one using Decoder#or

implicit val decoder: Decode[Camel] = decoderDerived or decoderCamelSnake

Decoder#or will try to decode using first decoder, and if it fails, then it will try out the second one.

Solution 2

If you are fine with having only camel_case input, then you might use @ConfiguredJsonCodec from "io.circe" %% "circe-generic-extras" % circeVersion package. Please note that to use this annotation you also need to include paradise compiler plugin.

addCompilerPlugin(
  "org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full
)
@ConfiguredJsonCodec
case class User(
  firstName: String,
  lastName: String
)

object User {
  implicit val customConfig: Configuration = Configuration.default.withSnakeCaseMemberNames
}

val userJson = User("John", "Doe").asJson
println(userJson)
// { "first_name" : "John", "last_name" : "Doe" } 

val decodedUser = decode[User](userJson.toString)
println(decodedUser)
// Right(User("John", "Doe"))

Also note that you don't need to write custom decoder & encoder derivers since that Configuration does that for you.

like image 74
Andrey Patseev Avatar answered Sep 20 '22 15:09

Andrey Patseev