Building Reads converter and case class at runtime in Play Framework - scala

I have a file that contains the following array of JSON objects:
[
{
"type": "home",
"number": 1111
},
{
"type": "office",
"number": 2222
},
{
"type": "mobile",
"number": 3333
}
]
In Play Framework 2.x I would define an implicit Reads converter to read the file and convert it to a Scala structure:
implicit val implicitRead : Reads[MyClass] = (
(JsPath \ "type").read[String] and
(JsPath \ "number").read[Int]
) (MyClass.apply _)
the Scala case class defined as:
case class MyClass (myType: String, myNumber: Int)
and parsing the JSON with:
val json = // file record content
json.validate[MyClass] match {
case s: JsSuccess[MyClass] => {
val myObject: MyClass = s.get
// do something with myObject
}
case e: JsError => {
// error handling flow
}
Now, my problem is that I know the structure of the JSON file only at runtime, not at compilation time. Is it possible to build both the implicit Reads converter and the case class at runtime?

Use case classes directly with play-json:
Change the case class to:
case class MyClass (`type`: String, number: Int)
Add the json-formatter to the companion object:
object MyClass {
implicit val format = Json.format[MyClass]
}
The validate function looks now:
val myClass = // file record content
json.validate[Seq[MyClass]] match {
case JsSuccess(myClasses, _) => myClasses
case e: JsError => // handle error case
}
That's all you need. If you are not happy with the parameter names, you can use a Wrapper case class.

Related

Scala Play Read: How to Flatten Json containing Array of arrays to model

I am looking for a way to define a Reads which allows me to map a JSON containing the following structure:
{
"offers": [
[
{
"id": "1234",
(etc.)
}
]
]
}
to model such case class TransportOffer(offers: List[Offer])
Unfortunately I haven't been able to do this yet. This is my code:
implicit val transportOfferReads: Reads[TransportOffer] = (
(JsPath \ "offers").read[List[List[Offer]]].flatMap(r => r.flatten)
)(TransportOffer.apply _)
In this case the flattening is not possible, as flatMap expects another Reads. How would I wrap the flattened List into another Reads?
Or is there a simpler way?
I'll present 3 options:
Flattening in a short reads:
case class Offer(id: String)
object Offer {
implicit val format: OFormat[Offer] = Json.format[Offer]
}
case class TransportOffer(offers: List[Offer])
object TransportOffer {
implicit val transportOfferReads: Reads[TransportOffer] =
(JsPath \ "offers").read[List[List[Offer]]].map(x => TransportOffer(x.flatten))
}
Then calling:
Json.parse(jsonString).validate[TransportOffer].foreach(println)
outputs:
TransportOffer(List(Offer(1234)))
Code run at Scastie
Explicitly writing Reads:
implicit val transportOfferReads: Reads[TransportOffer] = (json: JsValue) => {
json \ "offers" match {
case JsUndefined() =>
JsError("offers undefined")
case JsDefined(value) =>
value.validate[List[List[Offer]]].map(x => TransportOffer(x.flatten))
}
Code run at Scastie.
First transform the json, into the model you'd like. For that define a transformer:
val jsonTransformer = (__ \ "offers").json
.update(__.read[JsArray].map{o => {
JsArray(o.value.flatMap(_.asOpt[JsArray].map(_.value)).flatten)
}})
Then, assuming we have the case classes and their formatters:
case class Offer(id: String)
object Offer {
implicit val format: OFormat[Offer] = Json.format[Offer]
}
case class TransportOffer(offers: List[Offer])
object TransportOffer {
implicit val format: OFormat[TransportOffer] = Json.format[TransportOffer]
}
We can call:
Json.parse(jsonString).transform(jsonTransformer) match {
case JsSuccess(value, _) =>
value.validate[TransportOffer].foreach(println)
case JsError(errors) =>
println(errors)
???
}
Output is:
TransportOffer(List(Offer(1234)))
Code run at Scastie.

Handling nulls with json Play

I am trying to parse json with null values for some fields using Play library. There is a case class which represents the data:
case class Id(value: Int) extends AnyVal
case class Name(value: String) extends AnyVal
case class Number(value: Int) extends AnyVal
case class Data(id: Option[Id], name: Option[Name], number: Option[Number])
Here is how parsing currently works:
def parse(jsValue: JsValue): Try[Seq[Data]] = Try {
jsValue.as[JsArray].value
.flatMap { record =>
val id = Id((record \ "id").as[Int])
val name = Name((record \ "name").as[String])
val number = Number((record \ "number").as[Int])
Some(Data(Some(id), Some(name), Some(number)))
}
}
Parsing with specific data types doesn't handle null cases, so this implementation returns:
Failure(play.api.libs.json.JsResultException: JsResultException(errors:List((,List(JsonValidationError(List(error.expected.jsstring),WrappedArray()))))))
For the input data like this:
{
"id": 1248,
"default": false,
"name": null,
"number": 2
}
I would like to have something like this: Seq(Data(Some(Id(1248)), None, Some(Number(2))))
I am going to write the data into the database so I do not mind writing some null values for these fields.
How can I handle null values for fields in parsed json?
You can simply let the play-json library generate the Reads for your case classes instead of writing them manually:
import play.api.libs.json._
object Data {
implicit val reads: Reads[Data] = {
// move these into the corresponding companion objects if used elsewhere...
implicit val idReads = Json.reads[Id]
implicit val numberReads = Json.reads[Number]
implicit val nameReads = Json.reads[Name]
Json.reads[Data]
}
}
def parse(jsValue: JsValue): Try[Seq[Data]] = Json.fromJson[Seq[Data]](jsValue).toTry
That way, your code will work even in case you change the arguments of your case classes.
If you still want to code it manually, you can use the readNullable parser:
val name: Option[Name] = Name(record.as((__ \ "name").readNullable[String]))
Note, however, that using Try is somewhat frowned upon in FP and directly using JsResult would be more idiomatic.
If you are not locked to use play-json then let me show how it can be done easily with jsoniter-scala:
import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._
implicit val codec: JsonValueCodec[Seq[Data]] = JsonCodecMaker.make(CodecMakerConfig)
val json: Array[Byte] = """[
{
"id": 1248,
"default": false,
"name": null,
"number": 2
}
]""".getBytes("UTF-8")
val data: Seq[Data] = readFromArray(json)
println(data)
That will produce the following output:
List(Data(Some(Id(1248)),None,Some(Number(2))))
Here you can see an example how to integrate it with the Play framework.

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
}

How to feed JSON to CASE CLASS directly using functional programming in scala?

{
"cars": [{
"amount": 120.00,
"name": "Car1"
}, {
"amount": 245.00,
"name": "Car2"
}]
}
I am reading above JSON as following in my Controller
val body: JsObject = request.body.asInstanceOf[JsObject]
I am having following CASE CLASS
case class BIC(name: String, amount: Double)
I want to create List[BIC] objects by reading data from JSON [e.g. body] using Functional style
Use Play JSON.
Example:
case class Wrapper(cars: List[Bic])
case class BIC(name: String, amount: Double)
Then in your controller:
implicit val wrapperFormats = Json.format[Wrapper]
implicit val bICFormats = Json.format[BIC]
def postCars(): Action[JsValue] = Action(json.parse) { implicit request =>
request.body.validate[Wrapper] match {
case JsSuccess(obj, _) => {
//do something with obj.
}
case JsError(err) => {
BadRequest(
JsObject(
"error" -> err.toString
)
)
}
}
}
Please note that I am returning Action[JsValue] this is so JQuery will run success when using AJAX.
I hope this helps,
Rhys
another reference:
https://www.playframework.com/documentation/2.5.x/ScalaJsonCombinators
First, define two case classes for your model like this :
object Models {
case class Bic(name : String, amount : Double)
object Bic {
implicit val BicFormat = Json.format[Bic]
}
case class Cars(bics : List[Bic])
object Cars {
implicit val CarsFormat = Json.format[Cars]
}
}
You're using the Play Framework so you can use the JSON library.
In your controller, if you want to read the bics, you can do it like that :
def getCars = Action(parse.json) { request =>
request.body.validate[Cars] map { cars =>
// treat your cars ..
}
}

Mapping values to case class in json4s

Say I have the following:
[ {
"job_id": "1",
"status": "running"
},
{
"job_id": "0",
"status": "finished"
}]
Could I somehow do the following with json4s:
case class Job(job_id: Int, status: JobStatus)
abstract class JobStatus
case class JobFinished extends JobStatus
case class JobRunning extends JobStatus
... some magic is probably needed here
Such that extracting the first snippet would result in:
[ Job(1, JobRunning()), Job(0, JobFinished())]
I think that the best way of creating a scala case class based on the JSON, is using this site, this add the magic, I normally use this site, you can even change the name of the clases so in your case, you can use the site and then manage the relations within classes:
JSON to Scala
This is possible, but needs some coding.
I will try to break it down in some smaller steps.
Defintion of types / model
// Types
case class Job(jobId: Int, status: JobStatus)
// Sealed trait to make match exhaustive in helper functions
sealed trait JobStatus
// use case object to not create uneeded instances, also case class without () no longer allowed
case object JobFinished extends JobStatus
case object JobRunning extends JobStatus
The "magic"
Needed imports
import org.json4s._
import org.json4s.native.Serialization
import org.json4s.native.Serialization.{read, write}
Helper Functions
// helper functions, could be improved by having a mapping
implicit def stringToJobStatus(in: String) : JobStatus = in match {
case "running" => JobRunning
case "finished" => JobFinished
}
implicit def jobStatusToString(jobStatus: JobStatus) : String = jobStatus match {
case JobRunning => "running"
case JobFinished => "finished"
}
Custom Serializer
// here is the "magic" a custom serializer
class JobSerializer extends CustomSerializer[Job](format => (
// unmarshal Function
{
case JObject( JField("job_id", JString(jobId)) :: JField("status", JString(status)) :: Nil ) => {
new Job(jobId.toInt, status)
}
},
// masrshal Function
{
case Job(jobId, status) => JObject(
JField("job_id", JString(jobId.toString)) ::
JField("status", JString(status)) :: Nil)
}
))
make Serializer acessible
// Implicit formats for serialization and deserialization
implicit val formats = Serialization.formats(NoTypeHints) + new JobSerializer
Example in REPL
val data = """
[
{
"job_id": "1",
"status": "running"
},
{
"job_id": "0",
"status": "finished"
}
]
"""
read[List[Job]](data)
res3: List[Job] = List(Job(1,JobRunning), Job(0,JobFinished))
Although #Andres Neumann's answer is pretty good it does require reimplementing the serialization of the whole Job class (which might be a lot larger than the dumbed-down Job class in the example), whereas the only serialization which is actually needed is for the status. Based on #Andreas' answer the actual code needed is somewhat shorter and does not require every field in the Job to be serialized manually.
// here is the "magic" a custom serializer
class JobStatusSerializer extends CustomSerializer[JobStatus](format => (
// unmarshal Function
{
case JString(status) => {
// helper functions, could be improved by having a mapping
def stringToJobStatus(in: String): JobStatus = in match {
case "running" => JobRunning
case "finished" => JobFinished
}
stringToJobStatus(status)
}
},
// marshal Function
{
case status: JobStatus => {
def jobStatusToString(jobStatus: JobStatus): String = jobStatus match {
case JobRunning => "running"
case JobFinished => "finished"
}
JString(jobStatusToString(status))
}
}
)
)
You could use an enum and add EnumSerializer to your formats through json4s-ext.
However, your enum would be serialized as an int (0 or 1 in your case).
Adding to my own answer, you can also use EnumNameSerializer which would serialize to the enum values you specified (like "running" or "finished").