Search code examples
scalacirce

Circe parse json from snake case keys


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


Solution

  • 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.