How to send Json from client with missing fields f

2019-07-06 01:49发布

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": []
}

2条回答
Summer. ? 凉城
2楼-- · 2019-07-06 02:14

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")
}
查看更多
Evening l夕情丶
3楼-- · 2019-07-06 02:30

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.

查看更多
登录 后发表回答