Reputation: 76067
I'm writing some Scala classes to model the result of a JSON API (which is not under my control).
I'm using Play 2, and the most straightforward way to do it seems to use case classes and make the names of the arguments match those of the keys in the JSON. But there's a problem, the JSON objects have the keys in TitleCase, whereas in Scala good style dictates that arguments and attributes are in camelCase (and TitleCase is reserved to classes, as in Java).
I've thought about creating a Reads
object that maps the TitleCase keys to the order of the arguments in the constructor of the case class, but that looks cumbersome to keep in sync and I'd need to basically write all the definitions twice. So that is mostly discarded.
Another option could be to preprocess the JSON to convert all keys to camelCase and then feed that to the default Reads
. But I don't know how to do this in Play yet (I guess that it should be just a recursive function that takes a JSObject
, and produces another, but this might be not very efficient, as it would need to re-create the whole JSON mapping... Maybe there's an efficient way to do that using Jackson's streaming API).
And finally the last option is to live with it and let the keywords to be in TitleCase, even if that is meant for classes and singleton objects only.
I bet that this problem is pretty recurrent (maybe if not with TitleCase with underscore_instead_of_spaces) what is the idiomatic way to handle this cases in Scala and Play framework?
Upvotes: 3
Views: 545
Reputation: 834
Your post mentioned you'd thought about building a Reads based on mapping order of arguments; that'd certainly be cumbersome. Did you look into creating a custom Read using the JSON Combinators that were introduced in Play! 2.1? The burden is a bit lower there; it's order insensitive
### Case Class
case class User(id: Option[Long] = None, username: String, firstName: String,
lastName: String, email: String, password: String)
### Reads
implicit val userReads: Reads[User] = (
(__ \ 'username).read[String] and
(__ \ 'firstname).read[String] and
(__ \ 'lastname).read[String] and
(__ \ 'email).read[String](email) and
(__ \ 'password).read[String]
) {
(username: String, firstName: String, lastName: String, email: String,
password: String) =>
User(None, username, firstName, lastName, email, password)
}
You can see here that I'm mapping a fully lowercase json object to a case class with camelcase
One of the things that's great about a custom reads combinator is you gain the ability to take advantage of some custom validators, such as email, which applies a 'must pass an email regex' requirement to my email as part of the object validation process.
This Reads will validate JSON as long as it has all the required fields in the right capitalization (that part will have to stay in sync). The rest of it is completely malleable; it's order insensitive.
Unfortunately it doesn't address your issue with keeping the two definitions in sync; some tooling might be a help there but it wouldn't solve the core problems.
Upvotes: 4