3

I have a case Class and its companion object like below. Now, when I send JSON without id, createdAt and deletedAt fields, because I set them elsewhere, I get [NoSuchElementException: JsError.get] error. It's because I do not set above properties.

How could I achieve this and avoid getting the error?

case class Plan(id: String,
                companyId: String,
                name: String,
                status: Boolean = true,
                @EnumAs planType: PlanType.Value,
                brochureId: Option[UUID],
                lifePolicy: Seq[LifePolicy] = Nil,
                createdAt: DateTime,
                updatedAt: DateTime,
                deletedAt: Option[DateTime]
                )

object Plan {
   implicit val planFormat = Json.format[Plan]
   def fromJson(str: JsValue): Plan = Json.fromJson[Plan](str).get
   def toJson(plan: Plan): JsValue = Json.toJson(plan)
   def toJsonSeq(plan: Seq[Plan]): JsValue = Json.toJson(plan)
}

JSON I send from client

{
    "companyId": "e8c67345-7f59-466d-a958-7c722ad0dcb7",
    "name": "Creating First Plan with enum Content",
    "status": true,
    "planType": "Health",
    "lifePolicy": []
}
Karol S
  • 9,028
  • 2
  • 32
  • 45
curious
  • 2,908
  • 15
  • 25

2 Answers2

1

You can introduce another case class just to handle serialization from request: like this

  case class NewPlan(name: String,
            status: Boolean = true,
            @EnumAs planType: PlanType.Value,
            brochureId: Option[UUID],
            lifePolicy: Seq[LifePolicy] = Nil        
            ) 

and then use this class to populate your Plan class.

grotrianster
  • 2,468
  • 14
  • 14
  • I have several classes like this, so for every class i will have to make a new class. is there any other solution? – curious Aug 07 '14 at 13:16
  • 1
    @acjay recommends some other solution, but only this one is clean and solid, others requires some changes to json formatters or making id of you model Optional - you do not want to do this, it may lead to hard to find bugs. Keep your domain model clean. – grotrianster Aug 07 '14 at 13:48
  • @grotrianster Respectfully, I think all of the options have their place. If (supposing) the first thing the code does when it encounters a new plan is create a real `id` for it, and this is the same situation for a whole bunch of models, there are definitely drawbacks to making and maintaining tightly coupled near-clones of every model. On the other hand, if there's a whole bunch of logic specifically for handling data in the `NewPlan` state, then maybe it makes sense to reify the incomplete state as a type in its own right, as you suggest. – acjay Aug 07 '14 at 13:54
  • @acjay it's always better to have more solution to the problem, we are not in a position to see full picture here, any solution can be valid :) – grotrianster Aug 07 '14 at 14:18
1

The fundamental issue is that by the time a case class is instantiated to represent your data, it must be well-typed. To shoe horn your example data into your example class, the types don't match because some fields are missing. It's literally trying to call the constructor without enough arguments.

You've got a couple options:

  • You can make a model that represents the incomplete data (as grotrianster suggested).
  • You can make the possible missing fields Option types.
  • You can custom-write the Reads part of your Format to introduce intelligent values or dummy values for the missing ones.

Option 3 might look something like:

// Untested for compilation, might need some corrections

val now: DateTime = ...

val autoId = Reads[JsObject] { 
  case obj: JsObject => JsSuccess(obj \ 'id match {
    case JsString(_) => obj
    case _ => obj.transform(
      __.update((__ \ 'id).json.put("")) andThen
      __.update((__ \ 'createdTime).json.put(now)) andThen
      __.update((__ \ 'updatedTime).json.put(now))
    )
  })
  case _ => JsError("JsObject expected")
}

implicit val planFormat = Format[Plan](
  autoId andThen Json.reads[Plan],
  Json.writes[Plan])

Once you do this once, if the issue is the same for all your other models, you can probably abstract it into some Format factory utility function.

This may be slightly cleaner for autoId:

val autoId = Reads[JsObject] {
  // Leave it alone if we have an ID already
  case obj: JsObject if (obj \ 'id).asOpt[String].isSome => JsSuccess(obj)
  // Insert dummy values if we don't have an `id`
  case obj: JsObject => JsSuccess(obj.transform(
    __.update((__ \ 'id).json.put("")) andThen
    __.update((__ \ 'createdTime).json.put(now)) andThen
    __.update((__ \ 'updatedTime).json.put(now))
  ))
  case _ => JsError("JsObject expected")
}
acjay
  • 34,571
  • 6
  • 57
  • 100