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.
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)))
}
Most helpful comment
Right, I think unfortunately right now your best bet on Scala 3 is something like this (tested on Scala 3):