Circe: Snake Case (or any Configuration-related problem) with Scala 3

Created on 10 Mar 2021  路  4Comments  路  Source: circe/circe

Hello, and thank you for all the effort you're putting in Circe, and Scala 3 migration.

I tried to measure the amount of effort needed to migrate one of my projects to Scala3.

I'm struggling a bit with JSON serialization since it involves snake_case transformations on case class attributes.
I used to use circe-generic-extras with ConfiguredJsonObject but I saw it will probably not be updated to Scala3 anytime soon, which I can understand.

Is there any other way (other than writing an Encoder by hand) to obtain an Encoder that uses snake_case for case class attributes?

Like deriving an Encoder and "mapping it" in some way?

Sorry if the question isn't clear enough, but I'm trying to find a workaround for this, and thanks again for your time and effort.

Most helpful comment

Right, I think unfortunately right now your best bet on Scala 3 is something like this (tested on Scala 3):

scala> import io.circe.{Encoder, JsonObject}, io.circe.generic.semiauto.deriveEncoder

scala> import java.util.regex.Pattern

scala> val snakeCaseTransformation: String => String = s => {
     |   val basePattern: Pattern = Pattern.compile("([A-Z]+)([A-Z][a-z])")
     |   val swapPattern: Pattern = Pattern.compile("([a-z\\d])([A-Z])")
     |   val partial = basePattern.matcher(s).replaceAll("$1_$2")
     |   swapPattern.matcher(partial).replaceAll("$1_$2").toLowerCase
     | }
val snakeCaseTransformation: String => String = Lambda$6404/700040865@2b523359

scala> def snakeCaseIfy[A](encoder: Encoder.AsObject[A]): Encoder.AsObject[A] =
     |   encoder.mapJsonObject(obj =>
     |     JsonObject.fromIterable(obj.toIterable.map {
     |       case (k, v) => (snakeCaseTransformation(k), v) }
     |     )
     |   )
     | 
def snakeCaseIfy
  [A](encoder: io.circe.Encoder.AsObject[A]): io.circe.Encoder.AsObject[A]

scala> case class Foo(firstName: String, lastName: String)
// defined case class Foo

scala> implicit val encoderFoo: Encoder.AsObject[Foo] = snakeCaseIfy(deriveEncoder)
val encoderFoo: io.circe.Encoder.AsObject[Foo] = io.circe.Encoder$$anon$66@717a0dd4

scala> import io.circe.syntax._

scala> Foo("Foo", "McBar").asJson
val res0: io.circe.Json = {
  "first_name" : "Foo",
  "last_name" : "McBar"
}

All 4 comments

You could use mapJson on a derived Encoder and run a snake_case transformation on each key in that Json object.

Right, I think unfortunately right now your best bet on Scala 3 is something like this (tested on Scala 3):

scala> import io.circe.{Encoder, JsonObject}, io.circe.generic.semiauto.deriveEncoder

scala> import java.util.regex.Pattern

scala> val snakeCaseTransformation: String => String = s => {
     |   val basePattern: Pattern = Pattern.compile("([A-Z]+)([A-Z][a-z])")
     |   val swapPattern: Pattern = Pattern.compile("([a-z\\d])([A-Z])")
     |   val partial = basePattern.matcher(s).replaceAll("$1_$2")
     |   swapPattern.matcher(partial).replaceAll("$1_$2").toLowerCase
     | }
val snakeCaseTransformation: String => String = Lambda$6404/700040865@2b523359

scala> def snakeCaseIfy[A](encoder: Encoder.AsObject[A]): Encoder.AsObject[A] =
     |   encoder.mapJsonObject(obj =>
     |     JsonObject.fromIterable(obj.toIterable.map {
     |       case (k, v) => (snakeCaseTransformation(k), v) }
     |     )
     |   )
     | 
def snakeCaseIfy
  [A](encoder: io.circe.Encoder.AsObject[A]): io.circe.Encoder.AsObject[A]

scala> case class Foo(firstName: String, lastName: String)
// defined case class Foo

scala> implicit val encoderFoo: Encoder.AsObject[Foo] = snakeCaseIfy(deriveEncoder)
val encoderFoo: io.circe.Encoder.AsObject[Foo] = io.circe.Encoder$$anon$66@717a0dd4

scala> import io.circe.syntax._

scala> Foo("Foo", "McBar").asJson
val res0: io.circe.Json = {
  "first_name" : "Foo",
  "last_name" : "McBar"
}

Awesome detailed help.
Thank you!

I'm sorry I only exposed the Serialization issue although I obviously have deserialization ones.

Is there a way to "unsakify" with a derivedDecoder?

Edit: https://gitter.im/circe/circe?at=5d542e282612bb718c685031 this withFocus seems like a good starting point for my investigations.

Something around these lines:

    private val snakePattern = "_([a-z\\d])".r

    private val snakeToCamel: String => String = s => {
        snakePattern.replaceAllIn(s, {m =>
            m.group(1).toUpperCase()
        })
    }

    private def unSnakeCaseIfy(o: JsonObject): JsonObject = 
        JsonObject.fromIterable(o.toVector.map { 
            case (k, v) => snakeToCamel(k) -> v 
        })

    def unSnakeCaseIfy[A](decoder: Decoder[A]): Decoder[A] = (c: HCursor) => {
        decoder.tryDecode(c.withFocus(_.mapObject(unSnakeCaseIfy)))
    }
Was this page helpful?
0 / 5 - 0 ratings

Related issues

LukaJCB picture LukaJCB  路  7Comments

travisbrown picture travisbrown  路  4Comments

ChetanBhasin picture ChetanBhasin  路  6Comments

allantl picture allantl  路  6Comments

jilen picture jilen  路  3Comments