RuntimeException on validation of JsonReaders - Scala - ReactiveMongo - json

I have a case classe to store my userOption that I insert into my User.
The structure of my user is as such:
{
"_id":ObjectId("55d54d05ece39a6cf774c3e4"),
"main":{
"providerId":"userpass",
"userId":"test1#email.com",
"firstName":"test",
"lastName":"one",
"fullName":"Test One",
"email":"test1#email.com",
"authMethod":{
"method":"userPassword"
},
"passwordInfo":{
"hasher":"bcrypt",
"password":"aslkdjfasdjh"
}
},
"userOption":{
"hotLeadNotification":{
"f":"IMMEDIATE"
}
}
}
now, I'd like to add an additional option: favoriteNotification.
I changed my case class adding favoriteNotification:
case class UserOption (
hotLeadNotification: Frequency = Frequency("IMMEDIATE"),
favoriteNotification: Frequency = Frequency("IMMEDIATE")
)
object UserOption{
implicit val userOptionFormat = Json.format[UserOption]
implicit object BSONObjectIDFormat extends Format[BSONObjectID] {
def writes(objectId: BSONObjectID): JsValue = JsString(objectId.toString())
def reads(json: JsValue): JsResult[BSONObjectID] = json match {
case JsString(x) => {
val maybeOID: Try[BSONObjectID] = BSONObjectID.parse(x)
if(maybeOID.isSuccess) JsSuccess(maybeOID.get) else {
JsError("Expected BSONObjectID as JsString")
}
}
case _ => JsError("Expected BSONObjectID as JsString")
}
}
val userOptionForm = Form(
mapping(
"hotLeadNotification" -> text,
"favoriteNotification" -> text
)((hotLeadNotification: String, favoriteNotification: String) =>
UserOption(
hotLeadNotification = Frequency(hotLeadNotification),
favoriteNotification = Frequency(favoriteNotification)
)
)((u:UserOption) => Some(u.hotLeadNotification.f, u.favoriteNotification.f))
)
implicit object UserOptionBSONReader extends BSONDocumentReader[UserOption] {
def read(doc: BSONDocument): UserOption =
UserOption(
doc.getAs[Frequency]("hotLeadNotification").getOrElse(Frequency("IMMEDIATE")),
doc.getAs[Frequency]("favoriteNotification").getOrElse(Frequency("IMMEDIATE"))
)
}
implicit object UserOptionBSONWriter extends BSONDocumentWriter[UserOption]{
def write(userOption: UserOption): BSONDocument =
BSONDocument(
"hotLeadNotification" -> userOption.hotLeadNotification,
"favoriteNotification" -> userOption.favoriteNotification
)
}
}
Since I added favoriteNotification, I get a RuntimeException:
java.lang.RuntimeException: (/userOption/favoriteNotification,List(ValidationError(error.path.missing,WrappedArray())))
at scala.sys.package$.error(package.scala:27) ~[scala-library-2.11.6.jar:na]
at play.api.libs.iteratee.Iteratee$$anonfun$run$1.apply(Iteratee.scala:355) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
at play.api.libs.iteratee.Iteratee$$anonfun$run$1.apply(Iteratee.scala:348) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
at play.api.libs.iteratee.StepIteratee$$anonfun$fold$2.apply(Iteratee.scala:670) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
at play.api.libs.iteratee.StepIteratee$$anonfun$fold$2.apply(Iteratee.scala:670) ~[play-iteratees_2.11-2.3.9.jar:2.3.9]
But there's not list in my code. What am I doing wrong?
Thanks for your help

The issue was that UserOption was an Option, but not it parameters. As I added only the new option is the case class and not in the database, it was throwing this error.
I changed the case class as adding options:
case class UserOption (
hotLeadNotification: Option[Frequency] = Some(Frequency("IMMEDIATE")),
favoriteNotification: Option[Frequency] = Some(Frequency("IMMEDIATE"))
)

Related

Json body converted to sealed trait

I have a Play! endpoint which can receive a json body as 3 or 4 forms (I tried using generic type, but not working).
Controller:
def getChartData = Action.async(parse.json) { request =>
// ERROR: can not cast JsValue to type ChartDataRequest (which is sealed trait)
service.getChartData(request.body.asInstanceOf[ChartDataRequest]).map {
data => Ok(Json.toJson(data))
}.recover {
case _ => InternalServerErrror
}
}
Service:
def getChartData(request: ChartDataRequest): Future[data_type] = {
if (request.isInstanceOf[PieChartRequest]) {
//
} else if (request.isInstanceOf[BarChartRequest]) {
//
} else {
//
}
}
dtos:
sealed trait ChartDataRequest
final case class PieChartRequest(field1: String, field2: String, ....)
extends ChartDataRequest
final case class BarChartRequest(field1: String, field2: String, ....)
extends ChartDataRequest
I found here the solution to use sealed traits, but can't do it well.
In this point, I can not convert the JsValue to ChartDataRequest type. I can use a field "classType" in my json and then using the match pattern to create the specified object (PieDataRequest or BarDataRequest) but I think this is not the best solution.
Inside all my controller methods where I send objects as json body, I use the play validator, but have the same problem, and I removed it from code.
// ChartDataRequest can have PieDataRequest or BarDataRequest type
request.body.validate[ChartDataRequest] match {
case JsSuccess(value, _) => // call the service
case JsError(_) => Future(BadRequest("Invalid json body"))
}
thanks
You can follow this:
sealed trait ChartDataRequest
final case class PieChartRequest(field1: String) extends ChartDataRequest
final case class BarChartRequest(field2: String) extends ChartDataRequest
final case object WrongData extends ChartDataRequest
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val ChartDataRequests: Reads[ChartDataRequest] = {
val pc = Json.reads[PieChartRequest]
val bc = Json.reads[BarChartRequest]
__.read[PieChartRequest](pc).map(x => x: ChartDataRequest) |
__.read[BarChartRequest](bc).map(x => x: ChartDataRequest)
}
def getChartData(request: ChartDataRequest) = {
request match {
case _: PieChartRequest =>
Future("PieChartRequest")(defaultExecutionContext)
case _: BarChartRequest =>
Future("BarChartRequest")(defaultExecutionContext)
case _ =>
Future("WrongData")(defaultExecutionContext)
}
}
def getChartDataAction = Action.async(parse.json) { request =>
// you can separate this to a new function
val doIt = request.body.asOpt[JsObject].fold[ChartDataRequest](
WrongData
){
jsObj =>
jsObj.asOpt[ChartDataRequest].fold[ChartDataRequest](
WrongData
)(identity)
}
getChartData(doIt).map {
data => Ok(Json.toJson(data))
}(defaultExecutionContext).recover {
case _ => InternalServerError
}(defaultExecutionContext)
}

Insert record into Db using Slick (Scala), Best practices for an Entity

First to say, I'm newcomer in Scala and really need a little help. I need to build a web api, and I'll try to insert one record into database, but have some problems with mapping the entity (db table) into a model (class). I worked with .Net Core Web API (there I used Entity Framework Core, here in Scala use Slick) and try to keep same arhitecture in Scala, but need some more informations, because on the internet I find a lot of versions, and can not choose the best.
As database, MySQL is used.
User.scala
case class User(
id: Int = 0,
userName: String,
firstName: String,
lastName: String
) {
override def equals(that: Any): Boolean = true
}
object User {
implicit object UserFormat extends Format[User] {
def writes(user: User): JsValue = {
val userSeq = Seq(
"id" -> JsNumber(user.id),
"userName" -> JsString(user.userName),
"firstName" -> JsString(user.firstName),
"lastName" -> JsString(user.lastName)
)
JsObject(userSeq)
}
def reads(json: JsValue): JsResult[User] = {
JsSuccess(User(
(json \ "id").as[Int].value,
(json \ "userName").as[String].value,
(json \ "firstName").as[String].value,
(json \ "lastName").as[String].value)
)
}
}
def tupled = (this.apply _).tupled
}
class UserMap #Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit ex: ExecutionContext) {
val dbConfig: DatabaseConfig[JdbcProfile] = dbConfigProvider.get[JdbcProfile]
val db: JdbcBackend#DatabaseDef = dbConfig.db
val dbUsers = TableQuery[UserDef]
def getAll(): Unit = {
val action = sql"SELECT Id, UserName, FirstName, LastName FROM Users".as[(Int, String, String, String)]
return db.run(action)
}
def add(user: User): Future[Seq[User]] = {
dbUsers += user
db.run(dbUsers.result)
}
}
UserDef.scala (which is a mapper of db table / entity)
class UserDef(tag: Tag) extends Table[User](tag, "Users") {
def id = column[Int]("Id", O.PrimaryKey, O.AutoInc)
def userName = column[String]("UserName")
def firstName = column[String]("FirstName")
def lastName = column[String]("LastName")
override def * = (id, userName, firstName, lastName) <> (create, extract)
def create(user: (Int, String, String, String)): User = User(user._1, user._2, user._3, user._4)
def extract(user: User): Option[(Int, String, String, String)] = Some((user.id, user.userName,user.firstName,user.lastName))
}
UsersController.scala
def createUser = Action(parse.json) { implicit request => {
val userJson = request.body
var user = new User(
-1,
(userJson \ "userName").as[String].value,
(userJson \ "firstName").as[String].value,
(userJson \ "lastName").as[String].value
)
var users = TableQuery[UserDef]
Await.result(db.run(DBIO.seq(
users += user,
users.result.map(println))), Duration.Inf
)
Ok(Json.toJson(user))
}
}
How I see the problem:
UserDef is an Entity and must remain clean, only table columns definitions
UserMap is the bridge between User class and UserDef (entity), can be used as a repository with crud methods (getAll(), getById(id), create(user), update(user), delete(id)). This is in same file as User class, but probably must be moved in another.
User class is the model and need to contain only their parameters and writes/reads (Scala specifics)
and now in the controller:
If I try to insert a record into database, with current method, first I need to get all rows from table, and then to add the new record in the list. What happening if I have 3 4mil records in this table? Will get all these rows useless to insert only a new row.
Then, after inserting this new row, I need to return it into client, but how I can get it updated (Id is every time -1, but if I get entire list to see what it contain, I can see the correct id for the newest entity)
thx
Finally, I found a good solution and post it here, maybe somebody need this:
UserMap, for me at least will become UserRepository. There I have CRUD operations and maybe some extra :
def getAll(): Future[Seq[User]] = {
db.run(dbUsers.result)
}
def getById(id: Int): Future[Option[User]] ={
val action = dbUsers.filter(_.id === id).result.headOption
db.run(action)
}
def create(user: User): Future[User] = {
val insertQuery = dbUsers returning dbUsers.map(_.id) into ((x, id) => x.copy(id = id))
val action = insertQuery += user
db.run(action)
}
def update(user: User) {
Try( dbUsers.filter(_.id === user.id).update(user)) match {
case Success(response) => db.run(response)
case Failure(_) => println("An error occurred!")
}
}
def delete(id: Int) {
Try( dbUsers.filter(_.id === id).delete) match {
case Success(response) => db.run(response)
case Failure(_) => println("An error occurred!")
}
}
and UsersController:
def getAll() = Action {
var users = Await.result(usersRepository.getAll(), Duration.Inf)
Ok(Json.toJson(users))
}
def getById(id: Int) = Action { implicit request => {
val user = Await.result(usersRepository.getById(id), Duration.Inf)
Ok(Json.toJson(user))
}
}
def create = Action(parse.json) { implicit request => {
val userJson = request.body
var user = new User(
-1,
(userJson \ "userName").as[String].value,
(userJson \ "firstName").as[String].value,
(userJson \ "lastName").as[String].value
)
var createdUser = Await.result(usersRepository.create((user)), Duration.Inf)
Ok(Json.toJson(createdUser))
}
}
def update(id: Int) = Action(parse.json) { implicit request => {
val userJson = request.body
var user = new User(
(userJson \ "id").as[Int].value,
(userJson \ "userName").as[String].value,
(userJson \ "firstName").as[String].value,
(userJson \ "lastName").as[String].value
)
var updatedUser = usersRepository.update(user)
Ok(Json.toJson(user))
}
}
def delete(id: Int) = Action {
usersRepository.delete(id)
Ok("true")
}
Anyway, I know I have some bad blocks of code there...especially in create & update methods, where convert json to User.
I wanted to give it a try, and here is a full working example of a Play 2.7/Scala 2.13/Slick 4.0.2 REST-API controller bound to a MySQL database.
Since you are starting with Scala, maybe it is a bit overwhelming at first to get eased with Play, Slick, etc...
So here is an humble skeleton (derived from Play-Slick GitHub)
So first, since we want to write an API, here is the conf/routes file:
GET /users controllers.UserController.list()
GET /users/:uuid controllers.UserController.get(uuid: String)
POST /users controllers.UserController.create()
PUT /users controllers.UserController.update()
DELETE /users/:uuid controllers.UserController.delete(uuid: String)
Nothing to fancy here, we just bind routes to functions in the upcoming controller.
Just notice that the 2nd GET and the DELETE expect an UUID as query param, while Json bodies with be used for the POST and PUT.
It would be nice to see the model right now, in app/models/User.scala:
package models
import java.util.UUID
import play.api.libs.json.{Json, OFormat}
case class User(
uuid: UUID,
username: String,
firstName: String,
lastName: String
) {
}
object User {
// this is because defining a companion object shadows the case class function tupled
// see: https://stackoverflow.com/questions/22367092/using-tupled-method-when-companion-object-is-in-class
def tupled = (User.apply _).tupled
// provides implicit json mapping
implicit val format: OFormat[User] = Json.format[User]
}
I used an uuid instead using a numerical id, but basically, it is the same.
Notice that a Json serializer/deserializer can be written in just one line (you don't need to detail it with case classes). I think it is also a good practice to not override it to produce Seq as found on your code, since this serializer will be very usefull when converting objects to Json on the controller.
Now the tupled definition is most likelly a hack (see comment) that will be required later on the DAO...
Next, we need a controller in app/controllers/UserController.scala:
package controllers
import java.util.UUID
import forms.UserForm
import javax.inject.Inject
import play.api.Logger
import play.api.data.Form
import play.api.i18n.I18nSupport
import play.api.libs.json.Json
import play.api.mvc._
import services.UserService
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
class UserController #Inject()(userService: UserService)
(implicit ec: ExecutionContext) extends InjectedController with I18nSupport {
lazy val logger: Logger = Logger(getClass)
def create: Action[AnyContent] = Action.async { implicit request =>
withFormErrorHandling(UserForm.create, "create failed") { user =>
userService
.create(user)
.map(user => Created(Json.toJson(user)))
}
}
def update: Action[AnyContent] = Action.async { implicit request =>
withFormErrorHandling(UserForm.create, "update failed") { user =>
userService
.update(user)
.map(user => Ok(Json.toJson(user)))
}
}
def list: Action[AnyContent] = Action.async { implicit request =>
userService
.getAll()
.map(users => Ok(Json.toJson(users)))
}
def get(uuid: String): Action[AnyContent] = Action.async { implicit request =>
Try(UUID.fromString(uuid)) match {
case Success(uuid) =>
userService
.get(uuid)
.map(maybeUser => Ok(Json.toJson(maybeUser)))
case Failure(_) => Future.successful(BadRequest(""))
}
}
def delete(uuid: String): Action[AnyContent] = Action.async {
Try(UUID.fromString(uuid)) match {
case Success(uuid) =>
userService
.delete(uuid)
.map(_ => Ok(""))
case Failure(_) => Future.successful(BadRequest(""))
}
}
private def withFormErrorHandling[A](form: Form[A], onFailureMessage: String)
(block: A => Future[Result])
(implicit request: Request[AnyContent]): Future[Result] = {
form.bindFromRequest.fold(
errors => {
Future.successful(BadRequest(errors.errorsAsJson))
}, {
model =>
Try(block(model)) match {
case Failure(e) => {
logger.error(onFailureMessage, e)
Future.successful(InternalServerError)
}
case Success(eventualResult) => eventualResult.recover {
case e =>
logger.error(onFailureMessage, e)
InternalServerError
}
}
})
}
}
So here:
basically, each of our 5 functions referenced from the routes file check input, and then delegate the work to an injected UserService (more on that later)
for the create and update functions, you can see that we use Play Forms that I think is also a good practice. Their role is to validate the incoming Json, and that Marshall it into a User type.
Also, you can see that we use Action.async: Scala offers a very powerfull leverage with Futures so lets use it! Basically by doing so, you ensure that your code is not-blocking, thus easing the IOPS on your hardware.
Finally for the case of GET (one), GET (all), POST and PUT, since we return users, and have a deseralizer, a simple Json.toJson(user) do the work.
Before jumping to service and dao, lets see the form, in app/forms/UserForm.scala:
package forms
import java.util.UUID
import models.User
import play.api.data.Form
import play.api.data.Forms.{mapping, nonEmptyText, _}
object UserForm {
def create: Form[User] = Form(
mapping(
"uuid" -> default(uuid, UUID.randomUUID()),
"username" -> nonEmptyText,
"firstName" -> nonEmptyText,
"lastName" -> nonEmptyText,
)(User.apply)(User.unapply)
)
}
Nothing too fancy here, just as the doc says, although there is just a trick : when no uuid is defined (in the POST case, then we generate one).
Now, the service... not so much required in this very case, but in practice it might be a good thing to have an extra layer (dealing with acls for example), in app/services/UserService.scala:
package services
import java.util.UUID
import dao.UserDAO
import javax.inject.Inject
import models.User
import scala.concurrent.{ExecutionContext, Future}
class UserService #Inject()(dao: UserDAO)(implicit ex: ExecutionContext) {
def get(uuid: UUID): Future[Option[User]] = {
dao.get(uuid)
}
def getAll(): Future[Seq[User]] = {
dao.all()
}
def create(user: User): Future[User] = {
dao.insert(user)
}
def update(user: User): Future[User] = {
dao.update(user)
}
def delete(uuid: UUID): Future[Unit] = {
dao.delete(uuid)
}
}
As you can see, here, it is just a wrapper around the dao, and finnally the dao in app/dao/UserDao.scala:
package dao
import java.util.UUID
import javax.inject.Inject
import models.User
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.db.NamedDatabase
import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}
class UserDAO #Inject()(#NamedDatabase("mydb") protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._
private val users = TableQuery[UserTable]
def all(): Future[Seq[User]] = db.run(users.result)
def get(uuid: UUID): Future[Option[User]] = {
db.run(users.filter(_.uuid === uuid).result.headOption)
}
def insert(user: User): Future[User] = {
db.run(users += user).map(_ => user)
}
def update(user: User): Future[User] = {
db.run(users.filter(_.uuid === user.uuid).update(user)).map(_ => user)
}
def delete(uuid: UUID): Future[Unit] = {
db.run(users.filter(_.uuid === uuid).delete).map(_ => ())
}
private class UserTable(tag: Tag) extends Table[User](tag, "users") {
def uuid = column[UUID]("uuid", O.PrimaryKey)
def username = column[String]("username")
def firstName = column[String]("firstName")
def lastName = column[String]("lastName")
def * = (uuid, username, firstName, lastName) <> (User.tupled, User.unapply)
}
}
So, here I have just adapted the code from the official play-slick example, so I guess, I do not have better comment than theirs...
Hope, the whole things helps to get a better picture :)
If something is unclear, feel free to ask!

What should a Play framework implicit val Writes[T] look like for super type?

What do I put instead of ??? so the code will type check? Or is there something else I should be doing? I'm using Play to generate JSON for classes B, C, D that all extend A (Layer), but the code that tries to build the JSON only knows it has an A, not which subtype B, C or D.
class Layer
object Layer {
implicit val layerWrites = new Writes[Layer] {
def writes(x: Layer) = x match {
case a: CloudLayer => ???
case b: VerticalVisibility => ???
case c: SkyClear => ???
}
}
}
case class CloudLayer(coverage: String, base: Int) extends Layer
case class VerticalVisibility(height: Int) extends Layer
case class SkyClear() extends Layer
object CloudLayer {
implicit val cloudLayerWrites = new Writes[CloudLayer] {
def writes(x: CloudLayer) = Json.obj(
"layerType" -> "cloudLayer",
"coverage" -> x.cloudCoverage,
"base" -> x.base * 100
)
}
}
object VerticalVisibility {
implicit val verticalVisibilityWrites = new Writes[VerticalVisibility] {
def writes(x: VerticalVisibility) = Json.obj(
"layerType" -> "verticalVisibility",
"height" -> x.height * 100
)
}
}
object SkyClear {
implicit val skyClearWrites = new Writes[SkyClear] {
def writes(x: SkyClear) = Json.obj( "layerType" -> "skyClear" )
}
}
The easiest solution would be just to remove the implicit modifiers from the instances in the subclasses and then refer to them explicitly:
object Layer {
implicit val layerWrites = new Writes[Layer] {
def writes(x: Layer) = x match {
case a: CloudLayer => CloudLayer.cloudLayerWrites.writes(a)
case b: VerticalVisibility =>
VerticalVisibility.verticalVisibilityWrites.writes(b)
case c: SkyClear => SkyClear.skyClearWrites.writes(c)
}
}
}
You could also just scrap the individual instances and move their contents into the pattern match.
If you're feeling adventurous, Julien Richard-Foy has a pretty neat enhanced version of the Json.writes, etc. macros that works on sealed type hierarchies.

Spray: Marshalling UUID to JSON

I'm having some problems marshalling from UUID to JSON
def complete[T <: AnyRef](status: StatusCode, obj: T) = {
r.complete(status, obj) // Completes the Request with the T obj result!
}
^
The signature of my class:
trait PerRequest extends Actor
with Json4sSupport
with Directives
with UnrestrictedStash
with ActorLogging {
val json4sFormats = DefaultFormats.
This gives me :
"id": {
"mostSigBits": -5052114364077765000,
"leastSigBits": -7198432257767597000
},
instead of:
"id": "b9e348c0-cc7f-11e3-9c1a-0800200c9a66"
So, how can I add a UUID format to json4sFormats to marshall UUID's correctly?? In other cases I mix in with a trait that have this function:
implicit object UuidJsonFormat extends RootJsonFormat[UUID] {
def write(x: UUID) = JsString(x.toString)
def read(value: JsValue) = value match {
case JsString(x) => UUID.fromString(x)
case x => deserializationError("Expected UUID as JsString, but got " + x)
}
}
But here I'm not able to because I don't have declared a spray.json.RootJsonReader and/or spray.json.RootJsonWriter for every type T and does not compile. (See complete function T <: AnyRef)
Thanks.
I solved it! If someone has the same problem take a look here
I defined my own UUID Serializer as follows:
class UUIDSerializer extends CustomSerializer[UUID](format => (
{
case JObject(JField("mostSigBits", JInt(s)) :: JField("leastSigBits", JInt(e)) :: Nil) =>
new UUID(s.longValue, e.longValue)
},
{
case x: UUID => JObject(JField("id", JString(x.toString)))
}
))
And now it's working!

Polymorphically reading JSON with Json4s and custom serializer

I have the following class hierachy:
object Calendar {
trait DayType
case object Weekday extends DayType
case object Weekend extends DayType
case object Holiday extends DayType
}
trait Calendar {
def dateType(date: LocalDate): Calendar.DayType
}
class ConstantCalendar(dayType: Calendar.DayType) extends Calendar {
override def dateType(date: LocalDate) = dayType
}
case object DefaultCalendar extends ConstantCalendar(Calendar.Weekday)
case class WeekdaysCalendar(defaults: Array[Calendar.DayType]) extends Calendar {
override def dateType(date: LocalDate) = defaults(date.getDayOfWeek - 1)
}
case class CustomCalendar(defaultCalendar: Calendar = DefaultCalendar,
dates: Map[LocalDate, Calendar.DayType] = Map.empty)
extends Calendar {
private def defaultType(date: LocalDate) = defaultCalendar.dateType(date)
private val dateMap = dates.withDefault(defaultType)
override def dateType(date: LocalDate) = dateMap(date)
}
I have defined the following serializers:
class JsonFormats(domainTypeHints: TypeHints,
domainCustomSerializers: List[Serializer[_]] = Nil,
domainFieldSerializers: List[(Class[_], FieldSerializer[_])] = Nil)
extends DefaultFormats {
override val typeHintFieldName = "type"
override val typeHints = domainTypeHints
override val customSerializers = JodaTimeSerializers.all ++ domainCustomSerializers
override val fieldSerializers = domainFieldSerializers
}
class JsonCalendarSerializer extends CustomSerializer[CustomCalendar]( format => (
{
case JObject(JField("type", JString("CustomCalendar")) ::
JField("defaultCalendar", JString(defaultCalendar)) ::
JField("dates", dates) ::
Nil
) =>
CustomCalendar(defaultCalendar) // TODO dates
},
{
case cal: CustomCalendar =>
val dates = cal.dates.foldLeft(JObject()) { (memo, dt) =>
dt match {
case (d, t) => memo ~ (f"${d.getYear}%04d-${d.getMonthOfYear}%02d-${d.getDayOfMonth}%02d", t.toString)
}
}
("type" -> "CustomCalendar") ~
("defaultCalendar" -> cal.defaultCalendar) ~
("dates" -> dates)
}
))
implicit val jsonFormats = new JsonFormats(ShortTypeHints(List(Calendar.Weekday.getClass,
Calendar.Weekend.getClass,
Calendar.Holiday.getClass,
classOf[CustomCalendar])),
new JsonCalendarSerializer :: Nil)
I had to create a custom Serializer to get around the fact that, in Json4s, Map keys have to be Strings.
I have a file that might contain the data for some Calendar, but I don't know beforehand which Calendar type it is.
When I try the following:
val cal = CustomCalendar("default", Map(new LocalDate(2013, 1, 1) -> Calendar.Holiday))
val ser = Serialization.write(cal)
val cal2: Calendar = Serialization.read(ser)
I get:
org.json4s.package$MappingException: Do not know how to deserialize 'CustomCalendar'
at org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$mkWithTypeHint(Extraction.scala:444)
at org.json4s.Extraction$ClassInstanceBuilder$$anonfun$result$6.apply(Extraction.scala:452)
at org.json4s.Extraction$ClassInstanceBuilder$$anonfun$result$6.apply(Extraction.scala:450)
at org.json4s.Extraction$.org$json4s$Extraction$$customOrElse(Extraction.scala:462)
at org.json4s.Extraction$ClassInstanceBuilder.result(Extraction.scala:450)
at org.json4s.Extraction$.extract(Extraction.scala:306)
at org.json4s.Extraction$.extract(Extraction.scala:42)
at org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21)
at org.json4s.jackson.Serialization$.read(Serialization.scala:50)
So it seems that Json4s isn't able to find my serializer.
So... any hints? Either on how to get Json4s to serialize/deserialize Maps with non-String keys, or how to make this work?
Thanks!
In the end I implemented the JsonCalendarSerializer as follows:
class JsonCalendarSerializer extends CustomSerializer[CustomCalendar]( format => (
{
case JObject(JField("defaults", JString(defaults)) ::
JField("dates", JObject(dateList)) ::
Nil
) =>
val dates = dateList map {
case JField(dt, JString(t)) =>
val tp = t match {
case "Weekday" => Calendar.Weekday
case "Weekend" => Calendar.Weekend
case "Holiday" => Calendar.Holiday
}
(LocalDate.parse(dt), tp)
}
CustomCalendar(defaults, dates.toMap)
},
{
case cal: CustomCalendar =>
val dates = cal.dates.foldLeft(JObject()) { (memo, dt) =>
dt match {
case (d, t) => memo ~ (d.toString, t.toString)
}
}
(format.typeHintFieldName -> classOf[CustomCalendar].getSimpleName) ~
("defaults" -> cal.defaultCalendar) ~
("dates" -> dates)
}
))
I removed the JField("type"...) from the deserializer and fixed the serializer to call format.typeHintFieldName and classOf[CustomCalendar].getSimpleName, and that seemed to fix the problem.