从层次结构中接收对象列表的端点

Endpoint receiving a list of objects from a hierarchy

Akka Http 中的端点如下所示:

pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[SMS]) { sms =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}

短信定义为:

sealed trait Message
case class SMS(numFrom: String, message:String) extends Message
case class Email(emailFrom: String, message: String) extends Message

如果我想收到 SMS 的列表,我可以执行以下操作:

type SMSList = List[SMS]
...

pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[SMSList]) { listOfSMSs =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}

如果我想同时收到短信和邮件列表怎么办? 我已经试过了,但没用:

type MessageList = List[Message]

pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[MessageList]) { listOfMessages =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}

是否可以接收属于同一层次结构的对象列表?

图书馆:

circe = 0.13.0
heikoseeberger = 1.35.3
akka http = 10.2.3

Json:

[ 
  {"numForm": "123 456", "message": "sms message"},
  {"emailFrom": "some@mail.com", "email message"}
]

假设您使用的是 akka-http 中的默认 json 序列化库 - spray-json - 您在组合多个 json reader 时受到很大限制s(根据 official page 和源代码)。你能做的最好的可能是为 Message.

手动编写一些格式化程序(或只是 reader)
import spray.json.DefaultJsonProtocol._
import spray.json._

implicit val smsFormat: JsonFormat[SMS] = jsonFormat2(SMS)
implicit val emailFormat: JsonFormat[Email] = jsonFormat2(Email)

implicit val messageFormat: JsonFormat[Message] = new JsonFormat[Message] {
  override def read(json: JsValue): Message = json match {
    case sms@JsObject(_) if sms.fields.contains("numFrom") => smsFormat.read(sms)
    case email@JsObject(_) if email.fields.contains("emailFrom") => emailFormat.read(email)
    case _ => deserializationError("object expected")
  }

  override def write(obj: Message): JsValue = obj match {
    case sms: SMS => sms.toJson
    case email: Email => email.toJson
    case _ => throw new RuntimeException("Houston, we have a problem")
  }
}

我还建议您看一下 circe 库,它的代码组合性更强。它也很容易与 akka-http 集成。


更新 1:(在指定确切的库之后):

选项很少:

  1. 合并几个解码器

      import io.circe.generic.auto._
      import io.circe.{Decoder, HCursor}
    
      class CirceExample extends App {
    
        sealed trait Message
    
        case class SMS(numFrom: String, message: String) extends Message
    
        case class Email(emailFrom: String, message: String) extends Message
    
        val smsDecoder = implicitly[Decoder[SMS]]
        val emailDecoder = implicitly[Decoder[Email]]
    
        val messageDecoder: Decoder[Message] = (c: HCursor) => smsDecoder(c).orElse(emailDecoder(c))
      }
    

    这很容易实现,因为Decoder的解码结果是Either

  2. 创建自定义解码器,明确检查所需字段 - documentation。这种方法有点类似于前面使用 spray-json.

    的示例

更新 2:

import cats.syntax.functor._
import io.circe.Decoder
import io.circe.generic.auto._
import io.circe.parser._

object CirceExample extends App {

  sealed trait Message

  case class SMS(numFrom: String, message: String) extends Message

  case class Email(emailFrom: String, message: String) extends Message

  implicit val messageDecoder: Decoder[Message] = List[Decoder[Message]](Decoder[SMS].widen, Decoder[Email].widen).reduceLeft(_ or _)
  // or without list..
  //implicit val messageDecoder: Decoder[Message] = Decoder[SMS].widen or Decoder[Email].widen

  val payload = """[{"emailFrom":"a","message":"b"}]"""
  val result = decode[List[Message]](payload)
  println(result)
}

这里有几个步骤,所以我会尽量保持简单。

首先,我们需要为 EmailSMS:

定义编码器和解码器
sealed trait Message
@JsonCodec case class SMS(numFrom: String, message:String) extends Message
@JsonCodec case class Email(emailFrom: String, message: String) extends Message

object Message {
  implicit val decodeA: Decoder[Message] = Decoder[SMS].map[Message](identity).or(Decoder[Email].map[Message](identity))
  implicit val encodeA: Encoder[Message] = Encoder.instance {
    case b @ SMS(_, _) => b.asJson
    case c @ Email(_, _) => c.asJson
  }
}

我在 this link 中找到了这个,之前该线程的所有尝试都没有成功。

让我们继续。我们必须在单独的模块中找到它。否则,我们会得到 enable macro paradise to expand macro annotations 错误。看完 post,基本上解释了宏不能在同一个模块中声明和使用,我将上面的代码移动到另一个模块中,使代码能够编译。

有了上面代码的独立模块后,我们需要定义一个新模块,以依赖第一个模块。

现在,在最后一个模块中,我们需要首先导入:

import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport.unmarshaller

然后我们可以创建一个路由:

val routes = pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[List[Message]]) { listOfMessages =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}