How to dynamically select class type when parsing JSON to object in Play Framework? - scala

The following sample code uses Play Framework to parse JSON to an object:
def createEvent(): Action[JsValue] = Action.async(parse.tolerantJson) {
request => {
request.body.validate[SomeEvent] match {
case o:JsSuccess[SomeEvent] => {
//do something
Future(Created)
}
}
}
}
Is it possible to generalise it so it can handle different event types? e.g.
def createEvent(): Action[JsValue] = Action.async(parse.tolerantJson) {
request => {
val eventType = request.contentType match {
case Some("SomeEventType") => SomeEvent
case Some("OtherEventType") => OtherEvent
}
request.body.validate[eventType] match {
case o:JsSuccess[eventType] => {
//do something
Future(Created)
}
}
}
}
Currently, the above code will fail in the line request.body.validate[eventType]

You can extract body.validate[T] into a function and call it from your patten matching construct with a proper type, i.e:
def extract[T: JsonFormat](implicit req: Request[AnyContent]) = req.body.valudate[T]
request.contentType match {
case Some("SomeEventType") => extract[SomeEvent]
case Some("OtherEventType") => extract[OtherEvent]
}

You can read and create class from contentType dynamically. But you can have problem, if will be no implicit Format for extracted type in scope:
error: No Json formatter found for type A. Try to implement an implicit Format for this type.
or
java.lang.ClassNotFoundException: models.Bazz
The available data:
model
package models
case class Bar(name: String)
request
request.contentType: Option[String] = Some("models.Bar")
incoming body
request.body: JsValue = """{"name": "bar name"}"""
format[Bar]
implicit val BarFormat = Json.format[models.Bar]
extractor:
def extract[A <: Any](ct: String, json: JsValue)(implicit format: Format[A]) = {
val manifest:Manifest[A] = Manifest.classType[A](Class.forName(ct))
json.validate[A]
}
request.contentType.map(extract(_, request.body))
res1: Option[play.api.libs.json.JsResult[models.Bar]] = Some(JsSuccess(Bar(bar name),/name))
you can see https://github.com/strongtyped/fun-cqrs/blob/demos/shop-sample/samples/shop/src/main/scala/funcqrs/json/TypedJson.scala for other way of deserializing self-contained json.

Related

Comparing the json data types at runtime using Jackson and Scala

I have an incoming JSON data that looks like below:
{"id":"1000","premium":29999,"eventTime":"2021-12-22 00:00:00"}
Now, I have created a class that will accept this record and will check whether the data type of the incoming record is according to the data types defined in the case class. However, when I am calling the method it is always calling the Failure part of the match case.
case class Premium(id: String, premium: Long, eventTime: String)
class Splitter extends ProcessFunction[String, Premium] {
val outputTag = new OutputTag[String]("failed")
def fromJson[T](json: String)(implicit m: Manifest[T]): Either[String, T] = {
Try {
println("inside")
lazy val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
mapper.readValue[T](json)
} match {
case Success(x) => {
Right(x)
}
case Failure(err) => {
Left(json)
}
}
}
override def processElement(i: String, context: ProcessFunction[String, Premium]#Context, collector: Collector[Premium]): Unit = {
fromJson(i) match {
case Right(data) => {
collector.collect(data)
println("Good Records: " + data)
}
case Left(json) => {
context.output(outputTag, json)
println("Bad Records: " + json)
}
}
}
}
Based on the sample record above, it should pass the Success value but no matter what I pass, it always enters the Failure part. What else is missing?
I am using Scala 2.11.12 and I tried examples from this link and this link but no luck.

How to make only few datatype which is not related to each other acceptable by generics

There is a trait which works perfectly. However, I would like to refactor the part related to generic [T] in order to limit the data type which could be accepted by generic [T] (I need only Option[JsValue] , JsValue , StringEnumEntry , String ). Is it possible to solve this problem through shapeless coproduct? Maybe there are other solutions?
trait ParameterBinders extends Log {
def jsonBinder[T](json: T, jsonType: java.lang.String = "json"): ParameterBinderWithValue = {
val jsonObject = new PGobject()
jsonObject.setType(jsonType)
json match {
case json: Option[JsValue] =>
jsonObject.setValue(json.map(Json.stringify).orNull)
case json: JsValue =>
jsonObject.setValue(Json.stringify(json))
case json: StringEnumEntry =>
jsonObject.setValue(json.value)
case json: String =>
jsonObject.setValue(json)
case _ =>
logger.error("unexpected data type ")
}
if (jsonType == "JSONSCHEMATYPE" || jsonType == "SYSPROPERTYTYPE") {
ParameterBinder(this, (ps, i) => {
ps.setObject(i, jsonObject)
})
} else {
ParameterBinder(json, (ps, i) => {
ps.setObject(i, jsonObject)
})
}
}
}
The easiest way is to use an ADT as described in the link of the first comment.
If you don't want to change the types that are accepted in jsonBinder then you can solve the problem by using a typeclass.
e.g.
trait JsonBindValue[T] {
def value(t: T): String
}
you would then have to provide instances for your accepted datatypes
object JsonBindValue {
implicit val OptJsBinder = new JsonBindValue[Option[JsValue]] {
def value(t: Option[JsValue]): String = {
t.map(Json.stringify).orNull
}
}
... more instances here
}
finally your function would look like this:
def jsonBinder[T : JsonBindValue](json: T, jsonType: java.lang.String = "json"): ParameterBinderWithValue = {
val binder = implicitly[JsonBindValue[T]]
jsonObject.setType(jsonType)
jsonObject.setValue(binder.value(json))
...
}
if you call the function without a implicit instance in scope you will get a compile time error.

How can I serialize Sangria responses with json4s and Akka HTTP?

I'm working through a slight variation of Sangria's Getting Started, using Akka HTTP. I'm attempting to use json4s-jackson as the serializaltion lib, but am running in to some trouble getting the response I want.
Specifically, the serialized response I get is the JSON version of the (StatusCode, Node) tuple:
{
"_1": {
"allowsEntity": true,
"defaultMessage": "OK",
"intValue": 200,
"reason": "OK"
},
"_2": {
"data": {
"foo": {
"id": "1",
"name": "Foo"
}
}
}
}
The data portion is correct, but obviously I just want that and not the first element of the serialized tuple.
I'm using akka-http-json4s, so my trait with route looks like:
case class GraphQlData(query: String, operation: Option[String])
trait FooController {
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
val fooRoutes = post {
entity(as[GraphQlData]) { data =>
QueryParser.parse(data.query) match {
// query parsed successfully, time to execute it!
case Success(queryAst) =>
complete {
Executor
.execute(
SchemaDefinition.FooSchema,
queryAst,
new FooService,
operationName = data.operation
)
.map(OK -> _)
.recover {
case error: QueryAnalysisError => BadRequest -> error.resolveError
case error: ErrorWithResolver => InternalServerError -> error.resolveError
}
}
// can't parse GraphQL query, return error
case Failure(error) =>
complete(BadRequest -> error.getMessage)
}
}
}
implicit def executionContext: ExecutionContext
}
For the life of me I can't figure out what's wrong. I've been looking at sangria-akka-http-example but it seems to be exactly the same, with the exception of using spray-json instead of json4s.
Ideas? Thanks!
Ah, figured it out. I neglected to add
import sangria.marshalling.json4s.jackson._
to the trait defining the route. Adding it does the trick.
Just wanted to provide a quick update to this answer for the full GraphQLRequest. Now the variables are included in the request.
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import org.json4s._
import org.json4s.JsonAST.JObject
import sangria.marshalling.json4s.jackson._
case class GQLRequest(query: String, operationName: Option[String], variables: JObject)
trait SomeJsonSupport extends Json4sSupport {
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
}
trait GraphQLResource extends SomeJsonSupport{
implicit val timeout:Timeout
implicit val system:ActorSystem
import system.dispatcher
def graphqlRoute: Route =
(post & path("graphql")) {
entity(as[GQLRequest]) { requestJson =>
println(s"This is the requestJson = $requestJson")
graphQLEndpoint(requestJson)
}
} ~
get {
println(s"This is working")
getFromResource("graphiql.html")
}
def graphQLEndpoint(requestJson: GQLRequest): Route = {
val route = QueryParser.parse(requestJson.query) match {
case Success(query) =>
println(s"This is the query $query")
val vars = requestJson.variables match {
case jObj:JObject => jObj
case _ => JObject(List.empty)
}
val futureJValue = Executor.execute(clientSchema,
query,
NclhGqlRequest(this),
operationName = requestJson.operationName,
variables = vars)
val futureTupleStatusCodeJValue = futureJValue.map(OK -> _).recover {
case error: QueryAnalysisError => BadRequest -> error.resolveError
case error: ErrorWithResolver => InternalServerError -> error.resolveError
}
complete(futureTupleStatusCodeJValue)
case Failure(error) =>
complete(BadRequest, error.getMessage)
}
route
}

missing FromRequestUnmarshaller[Entity] on akka post route

Let me start by saying that i am very new to akka-http, none of the books i have covered the marsheling topic well. So it is bit of a blackbox for me. I was able to obtain the following (Un)Marsheller which is capable of returning both json and protobuf based on a request header.
This part of the code works fine and i have a get route defined in akka-http and it works fine.
trait PBMarshaller {
private val protobufContentType = ContentType(MediaType.applicationBinary("octet-stream", Compressible, "proto"))
private val applicationJsonContentType = ContentTypes.`application/json`
implicit def PBFromRequestUnmarshaller[T <: GeneratedMessage with Message[T]](companion: GeneratedMessageCompanion[T]): FromEntityUnmarshaller[T] = {
Unmarshaller.withMaterializer[HttpEntity, T](_ => implicit mat => {
case entity#HttpEntity.Strict(`applicationJsonContentType`, data) =>
val charBuffer = Unmarshaller.bestUnmarshallingCharsetFor(entity)
FastFuture.successful(JsonFormat.fromJsonString(data.decodeString(charBuffer.nioCharset().name()))(companion))
case entity#HttpEntity.Strict(`protobufContentType`, data) =>
FastFuture.successful(companion.parseFrom(CodedInputStream.newInstance(data.asByteBuffer)))
case entity =>
Future.failed(UnsupportedContentTypeException(applicationJsonContentType, protobufContentType))
})
}
implicit def PBToEntityMarshaller[T <: GeneratedMessage]: ToEntityMarshaller[T] = {
def jsonMarshaller(): ToEntityMarshaller[T] = {
val contentType = applicationJsonContentType
Marshaller.withFixedContentType(contentType) { value =>
HttpEntity(contentType, JsonFormat.toJsonString(value))
}
}
def protobufMarshaller(): ToEntityMarshaller[T] = {
Marshaller.withFixedContentType(protobufContentType) { value =>
HttpEntity(protobufContentType, value.toByteArray)
}
}
Marshaller.oneOf(protobufMarshaller(), jsonMarshaller())
}
}
the issue i am facing is on the post route.
(post & entity(as[PropertyEntity])) { propertyEntity =>
complete {
saveProperty(propertyEntity)
}
}
During compilation time, i get the following error
Error:(20, 24) could not find implicit value for parameter um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[PropertyEntity]
(post & entity(as[PropertyEntity])) { propertyEntity =>
I am not sure exactly what i am missing. Do i need to define an implicit FromRequestUnmarshaller ? if so what should it have?
i was able to hack something together that works for the moment, but i still don't know how to create a general Unmarshaller that can decode any ScalaPB case class
implicit val um:Unmarshaller[HttpEntity, PropertyEntity] = {
Unmarshaller.byteStringUnmarshaller.mapWithCharset { (data, charset) =>
val charBuffer = Unmarshaller.bestUnmarshallingCharsetFor(data)
JsonFormat.fromJsonString(data.decodeString(charBuffer.nioCharset().name()))(PropertyEntity)
/*PropertyEntity.parseFrom(CodedInputStream.newInstance(data.asByteBuffer))*/
}
}
even this i don't know how to have both decoders enabled at the same time. so i have commented one out.

I18n in Play Framework 2.4.0

Here is my routes file:
GET /:lang controller.Application.index(lang: String)
GET /:lang/news controller.Application.news(lang: String)
Note that all of them start with /:lang.
Currently, I write Application.scala as
def index(lang: String) = Action {
implicit val messages: Messages = play.api.i18n.Messages.Implicits.applicationMessages(
Lang(lang), play.api.Play.current)
Ok(views.html.index("title"))
}
In this way, I have to write as many implicit Messages as Action. Is there any better solution for this?
Passing just Lang is simpler option:
def lang(lang: String) = Action {
Ok(views.html.index("play")(Lang(lang)))
}
//template
#(text: String)(implicit lang: play.api.i18n.Lang)
#Messages("hello")
You can reuse some code by using action composition, define wrapped request and action:
case class LocalizedRequest(val lang: Lang, request: Request[AnyContent]) extends WrappedRequest(request)
def LocalizedAction(lang: String)(f: LocalizedRequest => Result) = {
Action{ request =>
f(LocalizedRequest(Lang(lang), request))
}
}
Now you are able to reuse LocalizedAction like this:
//template
#(text: String)(implicit request: controllers.LocalizedRequest)
#Messages("hello")
//controller
def lang(lang: String) = LocalizedAction(lang){implicit request =>
Ok(views.html.index("play"))
}
Finally, I solved this problem in the following way.
As #Infinity suggests, I defined wrapped request and action as:
case class LocalizedRequest(messages: Messages,
request: Request[AnyContent])
extends WrappedRequest(request)
object Actions {
def LocalizedAction(lang: String)(f: LocalizedRequest => Result) = {
Action { request =>
f(LocalizedRequest(applicationMessages(Lang(lang), current), request))
}
}
object Implicits {
implicit def localizedRequest2Messages(implicit request: LocalizedRequest): Messages = request.messages
}
}
Now I'm able to use LocalizedAction like this:
def lang(lang: String) = LocalizedAction(lang) { implicit request =>
Ok(views.html.index("play"))
}
However, in order to omit the implicit parameter of Messages, which should be a play.api.i18n.Messages, I added a line to my template as:
#import controllers.Actions.Implicits._