Is there a common approach to handle PATCH requests in REST API using circe library? By default, circe does not allow decoding partial JSON with only a part of the fields specified, i.e. it requires all fields to be set. You could use a withDefaults
config, but it will be impossible to know if the field you received is null
or just not specified. Here is a simplified sample of the possible solution. It uses Left[Unit]
as a value to handle cases when the field is not specified at all:
# possible payloads
{
"firstName": "Foo",
"lastName": "Bar"
}
{
"firstName": "Foo"
}
{
"firstName": null
}
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
import io.circe.{Decoder, HCursor}
case class User(firstName: Option[String], lastName: String)
// In PATCH request only 1 field can be specified. The rest could be omitted. Left represents `not specified`
case class PatchUserRequest(firstName: Either[Unit, Option[String]], lastName: Either[Unit, String])
object PatchUserRequest {
implicit val decode: Decoder[PatchUserRequest] = new Decoder[PatchUserRequest] {
final def apply(c: HCursor): Decoder.Result[PatchUserRequest] =
for {
// Here we handle `no field specified` error cases as Left[Unit]
foo <- c.downField("firstName").as[Option[String]] match {
case Left(noFieldSpecified) => Right(Left(()))
case Right(result) => Right(Right(result))
}
bar <- c.downField("lastName").as[String] match {
case Left(noFieldSpecified) => Right(Left(()))
case Right(result) => Right(Right(result))
}
} yield PatchUserRequest(foo, bar)
}
}
object Apis extends Directives {
var user = User("Foo", "Bar")
val create = path("user")(post(entity(as[User])(newUser => user = newUser)))
val patch = path("user")(patch(entity(as[PatchUserRequest])(patchRequest => patch(patchRequest))))
// If field is specified - update the record, ignore otherwise
def patch(request: PatchUserRequest) {
request.firstName.foreach(newFirstName => user = user.copy(firstName = newFirstName)
request.lastName.foreach(newlastName => user = user.copy(lastName = newlastName)
}
Is there a better way to handle PATCH requests (with nullable fields) instead of writing custom codec that falls back to no value
if field is not specified in the JSON payload? Thanks
Here's how I've done this kind of thing:
import io.circe.{Decoder, Encoder, FailedCursor, Json}
import java.util.UUID
sealed trait UpdateOrDelete[+A]
case object Missing extends UpdateOrDelete[Nothing]
case object Delete extends UpdateOrDelete[Nothing]
final case class UpdateWith[A](value: A) extends UpdateOrDelete[A]
object UpdateOrDelete {
implicit def decodeUpdateOrDelete[A](
implicit decodeA: Decoder[A]
): Decoder[UpdateOrDelete[A]] = Decoder.withReattempt {
// We're trying to decode a field but it's missing.
case c: FailedCursor if !c.incorrectFocus => Right(Missing)
case c => Decoder.decodeOption[A].tryDecode(c).map {
case Some(a) => UpdateWith(a)
case None => Delete
}
}
// Random UUID to _definitely_ avoid collisions
private[this] val marker: String = s"$$marker-${UUID.randomUUID()}-marker$$"
private[this] val markerJson: Json = Json.fromString(marker)
implicit def encodeUpdateOrDelete[A](
implicit encodeA: Encoder[A]
): Encoder[UpdateOrDelete[A]] = Encoder.instance {
case UpdateWith(a) => encodeA(a)
case Delete => Json.Null
case Missing => markerJson
}
def filterMarkers[A](encoder: Encoder.AsObject[A]): Encoder.AsObject[A] =
encoder.mapJsonObject(
_.filter {
case (_, value) => value != markerJson
}
)
}
And then:
import io.circe.generic.semiauto._
case class UserPatch(
id: Long,
firstName: UpdateOrDelete[String],
lastName: UpdateOrDelete[String]
)
object UserPatch {
implicit val decodeUserPatch: Decoder[UserPatch] = deriveDecoder
implicit val encodeUserPatch: Encoder.AsObject[UserPatch] =
UpdateOrDelete.filterMarkers(deriveEncoder[UserPatch])
}
And then:
scala> import io.circe.syntax._
import io.circe.syntax._
scala> UserPatch(101, Missing, Delete).asJson
res0: io.circe.Json =
{
"id" : 101,
"lastName" : null
}
scala> UserPatch(101, UpdateWith("Foo"), Missing).asJson
res1: io.circe.Json =
{
"id" : 101,
"firstName" : "Foo"
}
scala> io.circe.jawn.decode[UserPatch]("""{"id":1}""")
res2: Either[io.circe.Error,UserPatch] = Right(UserPatch(1,Missing,Missing))
This approach lets you model the intent more cleanly while still being able to use generic derivation to avoid most of the boilerplate of writing your codecs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With