1
votes

I'm using Salat library for serializing case classes as mongoDb objects. My Item.scala file looks like this:

case class Item(_id: String = (new ObjectId).toString, itemId: Int, var name: String, var active: Boolean) extends WithId {
  override def id: ObjectId = new ObjectId(_id)
}

object Item extends MongoDb[Item] with MongoDao[Item] {
  override def collectionName: String = "items"
}

object ItemJsonProtocol extends DefaultJsonProtocol {
  implicit val itemFormat = jsonFormat4(Item.apply)
}

Now, I'm using it to post the Item entities as Json via Spray HTTP. I'd want to invoke it as follows:

curl.exe -H "Content-Type: application/json" -X PUT -d "{\"itemId\":
1, \"active\":true, \"name\" : \"test\"}" http://localhost:8080/items/

hoping it would provide generated id if I don't provide one.

However, after invoking curl command I'm getting an error:

The request content was malformed: Object is missing required member '_id'

Is there any way to mark the _id field as optional without making the Option out of it (this field will always be set) and defining custom JsonFormat thus (de)serializing the object by myself?

I've read this post: https://stackoverflow.com/a/10820293/1042869, but I was wondering if there's any other way to do that as I got many cases of the _id fields. There was a comment too saying "you but you can give that field a default value in the case class definition, so if the field is not in the json, it will assign the default value to it.", but as you can see here it doesn't seem to work.

Best, Marcin

2
what if it was defined case class Item(_id: Option[String] = Some((new ObjectId).toString) ...?Gangstead
Well, I don't see the point of doing _id an Option if it's always there.Marcin
But it's not there if you want to use the same model for object creation as for object retrieval. The other option is to create a separate model for item creation (without an Id).Gangstead

2 Answers

0
votes

So I solved the problem by writing a custom RootJsonFormat::

  implicit object ItemJsonFormat extends RootJsonFormat[Item] {
    override def read(json: JsValue): Item = json.asJsObject.getFields("_id", "itemId", "name", "active") match {
      case Seq(JsString(_id), JsNumber(itemId), JsString(name), JsBoolean(active)) => Item(_id = _id, itemId = itemId.toInt, name = name, active = active)
      case Seq(JsNumber(itemId), JsString(name), JsBoolean(active)) => Item(itemId = itemId.toInt, name = name, active = active)
      case _ => throw new DeserializationException("Item expected")
    }
    override def write(obj: Item): JsValue = JsObject(
      "_id" -> JsString(obj._id),
      "itemId" -> JsNumber(obj.itemId),
      "name" -> JsString(obj.name),
      "active" -> JsBoolean(obj.active)
    )
  }

Basically what it does is it checks if we received the _id in json, if we did then we're using it to construct the object, and in other case keep the auto-generated id field.

One other thing which might cause some trouble but in my opinion deserves mentioning somewhere - if anyone has a problem with nested objects ("non-primitive" types) - I advise using .toJson in write def (like obj.time.toJson, where obj.time is jodatime's DateTime) and JsValue's .convertTo[T] def in read, like time = JsString(time).convertTo[DateTime]. In order for this to work there have to be defined implicit json formats for those "non-primitive" objects.

Best, Marcin

0
votes

I would use this solution:

case class Item(_id: Option[String], itemId: Int, var name: String, var active: Boolean)

implicit object ItemJsonFormat extends RootJsonFormat[Item] {
   override def read(value: JsValue) = {
     val _id = fromField[Option[String]](value, "_id")
     val itemId = fromField[Int](value, "itemId")
     val expires = fromField[Long](value, "expires")
     val name = fromField[String](value, "name")
     val active = fromField[Boolean](value, "active")
     Item(_id, itemId, name, active)
   }
   override def write(obj: Item): JsValue = JsObject(
     "_id" -> JsString(obj._id),
     "itemId" -> JsNumber(obj.itemId),
     "name" -> JsString(obj.name),
     "active" -> JsBoolean(obj.active)
   )
}

The advantage over the json.asJsObject.getFields solution is that you have better control over what gets accepted on the case of an undefined id. The example where that would fail is the following:

  • itemId is a string, same as id
  • id is defined but itemId is not

In this case the match case would interpret the specified id as a itemId and not catch the error.