1

An endpoint in Akka Http looks like this:

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

And SMS is defined as:

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

If I want to receive a list of SMS I can do the following:

type SMSList = List[SMS]
...

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

What if I want to receive a list of SMSs and Emails at the same time? I've tried this and it didn't work:

type MessageList = List[Message]

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

Is it possible to receive a list of objects that belongs to the same hierarchy?

Libraries:

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"}
]
Tomer Shetah
  • 7,646
  • 6
  • 20
  • 32
M.G.
  • 93
  • 6
  • Oops I forgot to mention libraries: circe(0.13.0) and heikoseeberger (1.35.3), akka http (10.2.3) – M.G. Feb 08 '21 at 19:34
  • Does this help you? https://stackoverflow.com/q/50457466/2359227 – Tomer Shetah Feb 08 '21 at 20:19
  • Thank you Tomer, but I think this is a different problem. In your code you see ""{ \n "Something": ... }. This is a different input, I don't have the string "Something". I've updated the text with the json of the call. Correct me if I'm wrong – M.G. Feb 08 '21 at 21:10

2 Answers2

2

Assuming that you are using the default one json serialisation library in akka-http - spray-json - you are quite restricted with combining several json readers (according to official page and source code). The best you can do is probably to write manually some formatter(or just reader) for Message.

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

I would also suggest you to take a look at circe library, whose code is much more composable. It's also easy to integrate it with akka-http.


Update 1: (after specifying exact library):

There are few options:

  1. combine few decoders

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

    This is easy to achieve, because result of decoding for Decoder is Either.

  2. Create custom decoder, which checks for needed fields explicitly - documentation. This approach is a bit similar to the previous example with spray-json.


update 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)
}
  • I forgot to mention which libraries I'm using, and in fact I'm using `circe`. – M.G. Feb 08 '21 at 19:40
  • I couldn't make it work. I adapted the code to my example, I create the object Message with the encoders inside, imported it, but it didn't worked ... – M.G. Feb 08 '21 at 21:30
  • It should work, because decoders in circe are composable. That is, if you declare List[Message], circe will take existing predefined decoder for list and will search for implicit decoder for Message (alternatively you can explicitly specify all decoders). You should check what is the resolution of decoders. In Intellij Idea IDE you can do this via "View" -> "Show implicit hints" menu. It would be helpful if you post the error here. – Serhii Shynkarenko Feb 08 '21 at 22:39
  • @M.G. Ok, you are right, it doesn't work. The details of it (and actually your case is here https://stackoverflow.com/questions/42165460/how-to-decode-an-adt-with-circe-without-disambiguating-objects). Added update on how it should look like. Be careful to explicitly specify which decoders you'll use. – Serhii Shynkarenko Feb 08 '21 at 23:08
  • Oh, the second update worked like a charm!! Thank you so much !! – M.G. Feb 09 '21 at 13:16
1

There are several steps here, so I'll try to keep it simple.

First, we need to define the encoders and decoders for Email andn SMS:

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

I have found this in this link, after all previous attempts from that thread didn't work.

Let's go on. We have to locate that in a separate module. Otherwise, we get enable macro paradise to expand macro annotations error. After reading this post, which basically explains that Macros cannot be declared and used i nthe same module, I've moved the above code into another module, which made the code to compile.

After having a separate module with the above code, we need to define a new module, to take dependency on the first one.

Now, in the last module, we need to first import:

import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport.unmarshaller

then we can create a route:

val routes = pathPrefix("somePath" / Segment) { someData =>
  post {
    entity(as[List[Message]]) { listOfMessages =>
      // some code here ...
      complete(StatusCodes.OK)
    }
  }
}
Tomer Shetah
  • 7,646
  • 6
  • 20
  • 32
  • I tried your code but it didn't compile, I have to define implicit Codecs for SMS and Email case classes. Then it worked fine, but only accept one object, it doesn't work with "entity(as[List[Message]]) { ...". Apart from that it's a nice code – M.G. Feb 11 '21 at 23:00