【问题标题】:Handling PATCH requests with Akka HTTP and circe for nullable fields使用 Akka HTTP 和 circe 处理 PATCH 请求以获取可空字段
【发布时间】:2020-05-10 16:51:50
【问题描述】:

是否有使用 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)
}

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

【问题讨论】:

    标签: scala akka-http circe


    【解决方案1】:

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

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

    解决此问题的一种方法是将字段包装在新类型中

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

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

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

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

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

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

    【讨论】:

      【解决方案2】:

      我是这样做的:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-10-29
        • 1970-01-01
        • 2016-08-15
        • 1970-01-01
        • 2018-08-28
        • 2021-05-12
        • 2017-10-03
        相关资源
        最近更新 更多