Search code examples
scalaplay-json

How to "reads" into a Scala Case Class given a Json object with key names that start with a capital letter


This question is based upon Scala 2.12.12

scalaVersion := "2.12.12"

using play-json

"com.typesafe.play" %% "play-json" % "2.9.1"

If I have a Json object that looks like this:

{
   "UpperCaseKey": "some value", 
   "AnotherUpperCaseKey": "some other value" 
}

I know I can create a case class like so:

case class Yuck(UpperCaseKey: String, AnotherUpperCaseKey: String) 

and follow that up with this chaser:

implicit val jsYuck = Json.format[Yuck]

and that will, of course, give me both reads[Yuck] and writes[Yuck] to and from Json.

I'm asking this because I have a use case where I'm not the one deciding the case of the keys and I've being handed a Json object that is full of keys that start with an uppercase letter.

In this use case I will have to read and convert millions of them so performance is a concern.

I've looked into @JsonAnnotations and Scala's transformers. The former doesn't seem to have much documentation for use in Scala at the field level and the latter seems to be a lot of boilerplate for something that might be very simple another way if I only knew how...

Bear in mind as you answer this that some Keys will be named like this:

XXXYyyyyZzzzzz

So the predefined Snake/Camel case conversions will not work.

Writing a custom conversion seems to be an option yet unsure how to do that with Scala.

Is there a way to arbitrarily request that the Json read will take Key "XXXYyyyZzzz" and match it to a field labeled "xxxYyyyZzzz" in a Scala case class?

Just to be clear I may also need to convert, or at least know how, a Json key named "AbCdEf" into field labeled "fghi".


Solution

  • I think that the only way play-json support such a scenario, is defining your own Format.

    Let's assume we have:

    case class Yuck(xxxYyyyZzzz: String, fghi: String)
    

    So we can define Format on the companion object:

    object Yuck {
      implicit val format: Format[Yuck] = {
        ((__ \ "XXXYyyyZzzz").format[String] and (__ \ "AbCdEf").format[String]) (Yuck.apply(_, _), yuck => (yuck.xxxYyyyZzzz, yuck.fghi))
      }
    }
    

    Then the following:

    val jsonString = """{ "XXXYyyyZzzz": "first value", "AbCdEf": "second value" }"""
    val yuck = Json.parse(jsonString).validate[Yuck]
    println(yuck)
    yuck.map(yuckResult => Json.toJson(yuckResult)).foreach(println)
    

    Will output:

    JsSuccess(Yuck(first value,second value),)
    {"XXXYyyyZzzz":"first value","AbCdEf":"second value"}
    

    As we can see, XXXYyyyZzzz was mapped into xxxYyyyZzzz and AbCdEf into fghi.

    Code run at Scastie.

    Another option you have, is to usd JsonNaming, as @cchantep suggested in the comment. If you define:

    object Yuck {
      val keysMap = Map("xxxYyyyZzzz" -> "XXXYyyyZzzz", "fghi" -> "AbCdEf")
      implicit val config = JsonConfiguration(JsonNaming(keysMap))
      implicit val fotmat = Json.format[Yuck]
    }
    

    Running the same code will output the same. Code ru nat Scastie.