1

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

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
Techmag
  • 1,383
  • 13
  • 24

2 Answers2

3

Just use provided PascalCase.

case class Yuck(
  upperCaseKey: String,
  anotherUpperCaseKey: String)

object Yuck {
  import play.api.libs.json._

  implicit val jsonFormat: OFormat[Yuck] = {
    implicit val cfg = JsonConfiguration(naming = JsonNaming.PascalCase)

    Json.format
  }
}

play.api.libs.json.Json.parse("""{
   "UpperCaseKey": "some value", 
   "AnotherUpperCaseKey": "some other value" 
}""").validate[Yuck]
// => JsSuccess(Yuck(some value,some other value),)

play.api.libs.json.Json.toJson(Yuck(
  upperCaseKey = "foo",
  anotherUpperCaseKey = "bar"))
// => JsValue = {"UpperCaseKey":"foo","AnotherUpperCaseKey":"bar"}
cchantep
  • 9,118
  • 3
  • 30
  • 41
  • Thank you for the example code -- that will be very useful for some other things I need to do. In this case however it only resolves the first letter, which at least gives me "better" fields names in the Scala Case Class. If does not however handle the ```XXXYyyyyZzzzzz``` which I would prefer to was converted to ```xxxYyyyyZzzzzz``` or the arbitrary ```XXXyyyyyZzzzzz``` which I would prefer to be ```xxxYyyyyZzzzzz``` – Techmag Feb 04 '21 at 21:02
  • 1
    You could replace `PascalCase` by whatever custom `JsonNaming` ... Aka a `String => String` – cchantep Feb 04 '21 at 23:35
2

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.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
  • The JsonNaming approach looks way way more self-explanatory and maintainable. Question: Does one have to define ALL keys or only the exceptions this way? Could it be mixed/matched with the CamelCase/PascalCase/SnakeCase conversion methods? (Say 10 keys with 2 needing exceptions and 8 could flow through with PascalCase?). – Techmag Feb 08 '21 at 14:59
  • 1
    @Techmag, you need to define all keys, because behind the scenes play transforms this map into an apply method. Therefore if there is a mismatch in the values count it won't compile. I don't think you can mix it in with any of the `Case` conversions, but if you think about it, it doesn't make sense to use both. Just create the mapping as you want it, instead of creating the map camel cased and then transform it to pascal case, or the other way around. – Tomer Shetah Feb 08 '21 at 15:53
  • 1
    I was only thinking of the combination in the case where you did not have to supply all the keys. If all keys are required then yes multiple transformations would make no sense. Thank you for the clarity on this. – Techmag Feb 09 '21 at 14:03