使用 Akka HTTP 和 circe 处理可空字段的 PATCH 请求

Handling PATCH requests with Akka HTTP and circe for nullable fields

是否有使用 circe 库在 REST API 中处理 PATCH 请求的通用方法?默认情况下,circe 不允许解码部分 JSON 仅指定部分字段,即它需要设置所有字段。您可以使用 withDefaults 配置,但无法知道您收到的字段是 null 还是未指定。这是可能解决方案的简化示例。它使用 Left[Unit] 作为值来处理根本未指定字段的情况:

# 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)
}

是否有更好的方法来处理 PATCH 请求(具有可为空的字段)而不是编写自定义编解码器,如果在 JSON 有效负载中未指定字段,则回退到 no value?谢谢

我觉得这里的中心问题(正如你提到的)是 Option[String] 表达了 2 个状态,而你实际上需要 3 个,即:

  • 值存在,并且非空
  • 值存在,并且为空
  • 值不存在

解决这个问题的一种方法是用新类型包装您的字段

case class PatchField[T](value: Option[T])

这将允许您以下列方式写下您的请求class:

case class PatchUserRequest (
    firstName: Option[PatchField[String]],
    lastName: Option[PatchField[String]]
)

这意味着您的有效负载现在将具有以下形式:

{
  "firstName": {"value" : "Foo" },
  "lastName": {"value" : "Bar" }
}

{
  "firstName": {"value": "Foo"}
}

{
  "firstName": {"value": null}
}

我不确定是否有办法强制 circe 区分 null 和根本不存在的值,但我觉得这可能是一个很好的折衷方案。

下面是我是如何完成这种事情的:

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
      }
    )
}

然后:

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])
}

然后:

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))

这种方法可以让您更清晰地建模意图,同时仍然能够使用泛型推导来避免编写编解码器的大部分样板文件。