Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to send Json from client with missing fields for its corresponding Case Class after using Json.format function

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": []
}
like image 282
curious Avatar asked Jun 08 '26 00:06

curious


2 Answers

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.

like image 168
grotrianster Avatar answered Jun 10 '26 05:06

grotrianster


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")
}
like image 26
acjay Avatar answered Jun 10 '26 04:06

acjay