Parsing two kinds of json with one case class? - scala

I have a case class in scala that needs to parse a json object. However the json object can look two different ways. Like this:
"hint": {
"structure": [
"HIDE"
]
}
Or like this:
"hint": {
"type": "1",
"template": "A"
}
I want to parse them both into the same case class in Scala using circe. I've trie doing something like this:
case class Hint(`type`:Option[String] = None,template:Option[String], structure: Option[List[String]])
object Hint {
implicit val hintJsonDecoder: Decoder[Hint] = deriveDecoder[Hint]
implicit val hintJsonEncoder: ObjectEncoder[Hint] = deriveEncoder[rHint]
}
But it seems like there should be a neater way of doing this so that I can just return say a list of strings in the case of the first instance, and just the type and template in the second instance. But I can't figure out how to do this using Circe.
Grateful for your help!

Maybe I'm not understand your question, but the use of Option gives you what you wan't. If you wanted a less verbose solution, you can use auto-derivation like as follows:
import io.circe.generic.auto._
import io.circe.parser.decode
case class HintContainer(hint: Hint)
case class Hint(`type`: Option[String], template: Option[String], structure: Option[List[String]])
object Sample extends App {
val testData1 =
"""
|{
| "hint": {
| "structure": [
| "HIDE"
| ]
| }
|}
|""".stripMargin
val testData2 =
"""
|{
| "hint": {
| "type": "1",
| "template": "A"
| }
|}
|""".stripMargin
println(decode[HintContainer](testData1))
println(decode[HintContainer](testData2))
}
Which gives:
Right(HintContainer(Hint(None,None,Some(List(HIDE)))))
Right(HintContainer(Hint(Some(1),Some(A),None)))

Related

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.

scala json parse and get a nested key and value

I have a json like below,
I have json like below I need to extract the value from them
{
"filed1": "value1",
"message": {
"payload": [{
"type": ["Extra","ID"],
info": {
"value": 8
}
}, {
"type": ["Free"],
info": {
"value": 99
}
}, {
"type": ["Actual"],
info": {
"value": 100
}
}]
},
"code": "0000"
}
{
"filed1": "value1",
"message": {
"payload": [{
"type": ["Extra", "new"],
"value": 9
}]
},
"code": "0001"
}
from the above two types of json .
If the input json has list of type keys then look for type field which has element Extra and get the value inside info
If the input json has one type key then check type field , if it has element Extra and get the direct va;ue
I am trying like below for type but it fails for list of types json, i.e first json input
import org.json4s._
import org.json4s.jackson.JsonMethods._
import org.json4s.JsonDSL._
val json = parse(myjsonString, true)
val field = compact(render(json \\ "type"))
val ok = field.contains("[\"Extra\"")
if(ok == true){
println("value " + compact(render(json \\ "value")))
}
You need to use json4s to do the work for you. Specifically, you need to use the extract method on the json to convert it into your particular data structure. Once you have done that you can process it as Scala types.
This is my attempt at matching the structure of your JSON with Scala case classes:
case class PayloadInfo(value: Int)
case class Payload(`type`: List[String], info: PayloadInfo)
case class Message(payload: List[Payload])
case class Full(filed1: String, message: Message, code: String)
implicit val formats = DefaultFormats
val json = parse(myjsonString, true)
val full = json.extract[Full]
val res = full.message.payload.filter(_.`type`.contains("Extra"))
[ The backticks around type are required because it is a reserved word ]
I may have got the types a bit wrong but this should give you the idea of how to go about it.
You can also process the JValues directly but that is going to be much messier.

Building Reads converter and case class at runtime in Play Framework

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.

Json4s - extract to correct trait implementation based on kind property

I have a trait that has many implementations. When in json format you can distinguish easly between them by kind property. Is there a some simple way to have json4s pick correct implementation based on that property?
I would prefer to avoid writing huge custom serializer to handle each case individually.
I was hoping something like this would work:
class ExampleTest extends WordSpecLike with Matchers {
implicit val formats: Formats = DefaultFormats + new CarSerializer
val teslaJson =
"""
|{
| "name": "Tesla",
| "kind": "electric",
| "batteryCapacity": 250.0
|}
""".stripMargin
val bmwJson =
"""
|{
| "name": "BMW",
| "kind": "petrol",
| "tankCapacity": 70.0
|}
""".stripMargin
val tesla = ElectricCar(
"Tesla", "electric", 250.0
)
val bmw = PetrolCar(
"BMW", "petrol", 70.0
)
"JSON4s" should {
"extract electric car" in {
parse(teslaJson).extract[ElectricCar] should be(tesla)
}
"extract petrol car" in {
parse(bmwJson).extract[PetrolCar] should be(bmw)
}
"extract electric car with CarSerializer" in {
parse(teslaJson).extract[Car] should be(teslaJson)
}
}
}
class CarSerializer extends CustomSerializer[Car](_ => ( {
case car: JObject =>
car \ "kind" match {
case JString(v) if v == "electric" => car.extract[ElectricCar]
case JString(v) if v == "petrol" => car.extract[PetrolCar]
}
}, {
case car: Car => Extraction.decompose(car)
}))
sealed trait Car {
def name: String
def kind: String
}
case class ElectricCar(
name: String,
kind: String,
batteryCapacity: Double
) extends Car
case class PetrolCar(
name: String,
kind: String,
tankCapacity: Double
) extends Car
Unfortunately it does not work as extract and decompose functions require implicit formats to be present but the serializer itself is meant to be part of the formats.

Flatten extranous fields when extracting using LiftJson or Json4s

I want to extract using LiftJson or Json4s the following Json (not quite but something similar) to the following case classes.
{
"data": [
{
"id": "1234",
"message": "Test",
"comments": {
"data": [
{
"id": "4321",
"content": "Test2",
}
]
}
}
The case classes:
case class A(id: String, message: string, comments: List[B])
case class B(id: String, content: String)
For the top level I can do: (val \ "data").extract[List[A]] to flatten the extra data field. But for the second level one, I don't see I way to use extract DIRECTLY.
Could I use a custom serializer (exemple here) or any of the following function (json4s) to remove the extraneous "data" field? Or any idea to make it simple?
def mapField(f: JField => JField): JValue
def transformField(f: PartialFunction[JField, JField]): JValue
Want I want to avoid is creating others intermidiate case class to extract the data, and then create the shown case class with it.
I found the solution a while ago, but haven't had time to reply. I was thinking backward, this is easy:
def transformListData(src: JValue, field: String): JValue = {
src.transformField {
case JField(x, v) if x == field => JField(field, v \ "data")
}
}
transformListData(json, "comments")
The following would remove the extra data and would flatten the list.