Spray JSON root object reader as array or object - json

I have domain model like:
sealed trait MyTrait
case class MyObject(a: String) extends MyTrait
case class MyArray(a: Seq(MyObject)) extends MyTrait
Example usages would look like:
// array
[{"a": "foo"}, {"a": "bar"}]
//object
{"a": "foobar"}
I want to write one Spray JSON reader for MyTrait like:
implicit val myTraitReader: RooJsonReader[MyTrait] = new RootJsonReader[MyTrait] {
override def read(json: JsValue): MyTrait = {
// MAGIC?
// I need to be able to distinguish between JsObject and JsArray
// If I do json.asJsObject this is a JsObject then :D
// there is not json.asJsArray?
// How to pattern match on this use case?
}
}
All questions are commented out. :D
Thanks!

How about something like below?
implicit val myTraitReader: RootJsonReader[MyTrait] = new RootJsonReader[MyTrait] {
import MyObjectProtocol._
import MyArrayProtocol._
override def read(json: JsValue): MyTrait = json match {
case jsObj: JsObject => ??? // Convert to MyObject here
case jsArr: JsArray => ??? // Convert to MyArray here
case _ => ??? // A JsNull here??
}
}

Related

Building a Json Format for a Case Class with Abstract Members

I am using the Play Framework and trying to build JSON validator for a class with abstract members. Shown below, the DataSource class is the base class which I am trying to validate the format against.
// SourceTypeConfig Trait.
trait SourceTypeConfig
final case class RDBMSConfig(...) extends SourceTypeConfig
object RDBMSConfig { implicit val fmt = Json.format[RDBMSConfig] }
final case class DirectoryConfig(
path: String,
pathType: String // Local, gcloud, azure, aws, etc.
) extends SourceTypeConfig
object DirectoryConfig { implicit val fmt = Json.format[DirectoryConfig] }
// FormatConfig trait.
trait FormatConfig
final case class SQLConfig(...) extends FormatConfig
object SQLConfig { implicit val fmt = Json.format[SQLConfig]}
final case class CSVConfig(
header: String,
inferSchema: String,
delimiter: String
) extends FormatConfig
object CSVConfig { implicit val fmt = Json.format[CSVConfig]}
// DataSource base class.
case class DataSource(
name: String,
sourceType: String,
sourceTypeConfig: SourceTypeConfig,
format: String,
formatConfig: FormatConfig
)
What I am hoping to accomplish:
val input: JsValue = Json.parse(
"""
{
"name" : "test1",
"sourceType" : "directory",
"sourceTypeConfig" : {"path" : "gs://test/path", "pathType" "google"},
"format" : "csv",
"formatConfig" : {"header" : "yes", "inferSchema" : "yes", "delimiter" : "|"}
}
"""
)
val inputResult = input.validate[DataSource]
What I am struggling with is building the DataSource object and defining its reads/writes/format. I would like it to contain a match based on the sourceType and format values that direct it to point towards the associated sourceTypeConfig and formatConfig's formats so it can parse out the JSON.
Instead of building a parser at the DataSource level, I defined parsers at the SourceConfig and FormatConfig levels, similar to what is shown below.
sealed trait SourceConfig{val sourceType: String}
object SourceConfig{
implicit val fmt = new Format[SourceConfig] {
def reads(json: JsValue): JsResult[SourceConfig] = {
def from(sourceType: String, data: JsObject): JsResult[SourceConfig] = sourceType match {
case "RDBMS" => Json.fromJson[RDBMSConfig](data)(RDBMSConfig.fmt)
case "directory" => Json.fromJson[DirectoryConfig](data)(DirectoryConfig.fmt)
case _ => JsError(s"Unknown source type: '$sourceType'")
}
for {
sourceType <- (json \ "sourceType").validate[String]
data <- json.validate[JsObject]
result <- from(sourceType, data)
} yield result
}
def writes(source: SourceConfig): JsValue =
source match {
case b: RDBMSConfig => Json.toJson(b)(RDBMSConfig.fmt)
case b: DirectoryConfig => Json.toJson(b)(DirectoryConfig.fmt)
}
}
}
Then, DataSource could be simply defined as:
object DataSource { implicit val fmt = Json.format[DataSource] }
Another option is to use play-json-derived-codecs library:
libraryDependencies += "org.julienrf" %% "play-json-derived-codecs" % "4.0.0"
import julienrf.json.derived.flat
implicit val format1: OFormat[RDBMSConfig] = Json.format[RDBMSConfig]
implicit val format2: OFormat[DirectoryConfig] = Json.format[DirectoryConfig]
implicit val format3: OFormat[SourceTypeConfig] = flat.oformat((__ \ "sourceType").format[String])

Condition JSON reads in scala

I have two classes that inherit from a common parent. I would like to have one common JSON reader in the parent that will return an appropriate child based on supplied JSON. It is easier to explain this with a sample snippet below;
import play.api.libs.json.{JsPath, JsonValidationError, Reads}
sealed abstract class Animal(sound: String)
case class Goat(hooves: String) extends Animal("meh")
case class Cat(needsMilk: Boolean) extends Animal("meow")
val json ="""{"type": "goat", "hooves": "All good for climbing trees"}"""
object Animal {
val isSupportedAnimal: Reads[String] =
Reads.StringReads.filter(JsonValidationError("Unsupported animal"))(str => {
List("goat", "cat").contains(str)
})
val animalReads: Reads[Animal] = ((JsPath \ "type").read[String](isSupportedAnimal) and
//if animal is cat, use the cat specific reads and return a cat object
//if animal is goat, use goat specific reads and return a goat
)()
}
Given the json in the snippet, I would like the have Goat object because the specified type is goat.
I am new to scala so I might be approaching the problem in a wrong way. Suggestions are welcome.
Use a Map:
sealed abstract class Animal(val sound: String) // You probably want a val here, btw
final case class Goat(hooves: String) extends Animal("meh")
final case class Cat(needsMilk: Boolean) extends Animal("meow")
object Animal {
val readers: Map[String, Reads[_ <: Animal]] = Map(
"goat" -> implicitly[Reads[Goat]],
"cat" -> implicitly[Reads[Cat]],
// Sidenote: Trailing commas ^ make future modification easy
)
// Bonus: Set[String] <: String => Boolean, so you get isSupportedAnimal for free
// val isSupportedAnimal: String => Boolean = readers.keys
implicit val animalReads: Reads[Animal] = new Reads[Animal] {
def reads(s: JsValue): JsResult[Animal] = {
val tpe = (s \ "type").as[String]
val read = readers.get(tpe)
read.map(_.reads(s)).getOrElse(JsError(s"Unsupported animal: $tpe"))
}
}
}
If you'd rather not have this boilerplate, you can look into this library (which uses shapeless).
You could try to make an custom reader like this:
implicit val animalReads = new Reads[Animal] {
def reads(js: JsValue): Animal = {
(js \ "type").as[String] match {
case "cat" => Cat( (js \ "needsMilk").as[Boolean] )
case "goat" => Goat( (js \ "hooves").as[String] )
case _ => throw new JsonValidationError("Unsupported animal")
}
}
}

spray-json. How to get list of objects from json

I am trying to use akka-http-spray-json 10.0.9
My model:
case class Person(id: Long, name: String, age: Int)
I get json string jsonStr with list of persons and try to parse it:
implicit val personFormat: RootJsonFormat[Person] = jsonFormat3(Person)
val json = jsonStr.parseJson
val persons = json.convertTo[Seq[Person]]
Error:
Object expected in field 'id'
Probably i need to create implicit object extends RootJsonFormat[List[Person]] and override read and write methods.
implicit object personsListFormat extends RootJsonFormat[List[Person]] {
override def write(persons: List[Person]) = ???
override def read(json: JsValue) = {
// Maybe something like
// json.map(_.convertTo[Person])
// But there is no map or similar method :(
}
}
P.S. Sorry for my english, it's not my native.
UPD
jsonStr:
[ {"id":6,"name":"Martin Ordersky","age":50}, {"id":8,"name":"Linus Torwalds","age":43}, {"id":9,"name":"James Gosling","age":45}, {"id":10,"name":"Bjarne Stroustrup","age":59} ]
I get perfectly expected results with:
import spray.json._
object MyJsonProtocol extends DefaultJsonProtocol {
implicit val personFormat: JsonFormat[Person] = jsonFormat3(Person)
}
import MyJsonProtocol._
val jsonStr = """[{"id":1,"name":"john","age":40}]"""
val json = jsonStr.parseJson
val persons = json.convertTo[List[Person]]
persons.foreach(println)

Json.writes[List[Foo]] as "map"

I have a case class Foo(bars: List[Bar]) who is rendered as json via Json inception as an object with an array :
{"bars": [
{
"key: "4587-der",
"value": "something"
}
]
}
But I want to render the bars: List[Bar] as a "map" where Bar.key is used as key :
{"bars":{
"4587-der": {
"value": "something"
}
}
}
How can I obtains that without modifying my case class Foo ?
Thanks a lot
You can define a Writes for Bar by extending Writes[Bar] and implementing a writes method for it:
case class Bar(key: String, value: String)
implicit object BarWrites extends Writes[Bar] {
def writes(bar: Bar): JsValue = Json.obj(
bar.key -> Json.obj("value" -> bar.value)
)
}
scala> Json.stringify(Json.toJson(Bar("4587-der", "something")))
res0: String = {"4587-der":{"value":"something"}}
For those that may be interested, here is a (somewhat) crude implementation of Reads[Bar]:
implicit object BarReads extends Reads[Bar] {
def reads(js: JsValue): JsResult[Bar] = js match {
case JsObject(Seq((key, JsObject(Seq(("value", JsString(value))))))) => JsSuccess(Bar(key, value))
case _ => JsError(Seq())
}
}
scala> Json.parse(""" [{"4587-der":{"value": "something"}}] """).validate[List[Bar]]
res11: play.api.libs.json.JsResult[List[Bar]] = JsSuccess(List(Bar(4587-der,something)),)
Edit, since the OP wants the Bars merged into an object rather than an array:
You'll also have to define a special Writes[List[Bar]] as well:
implicit object BarListWrites extends Writes[List[Bar]] {
def writes(bars: List[Bar]): JsValue =
bars.map(Json.toJson(_).as[JsObject]).foldLeft(JsObject(Nil))(_ ++ _)
}
scala> val list = List(Bar("4587-der", "something"), Bar("1234-abc", "another"))
list: List[Bar] = List(Bar(4587-der,something), Bar(1234-abc,another))
scala> Json.stringify(Json.toJson(list))
res1: String = {"4587-der":{"value":"something"},"1234-abc":{"value":"another"}}

Scala (spray) json serialization and deserialization issue

I'm using my own implicit implementation of JSON serializer and deserializer for my case object
My case class looks like (it's just a code snippet)
sealed trait MyTrait
case object MyCaseClass extends MyTrait
I want to write my own ser. and deser. of JSON for MyTrait
implicit val myTraitFormat = new JsonFormat[MyTrait] {
override def read(json: JsValue): MyTrait = json.asJsObject.getFields("value") match {
case Seq(JsString("TEST")) ⇒ MyCaseClass
case _ ⇒ throw new DeserializationException(s"$json is not a valid extension of my trait")
}
override def write(myTrait: MyTrait): JsValue = {
myTrait match {
case MyCaseClass => JsObject("value" -> JsString("TEST"))
}
}
}
Now my test is failing by throwing DeserializationException:
"The my JSON format" when {
"deserializing a JSON" must {
"return correct object" in {
val json = """{"value": "TEST"}""".asJson
json.convertTo[MyTrait] must equal (MyCaseClass)
}
}
}
Obviously json.asJsObject.getFields("value")can not be matched to Seq(JsString("TEST")). Maybe this is related to using traits?
But I have found example on official spray-json site https://github.com/spray/spray-json#providing-jsonformats-for-other-types
Any ideas how to properly match on field in JsObject?
Thanks!
Best
Write variable name instead of string:
override def read(json: JsValue): MyCaseClass = json.asJsObject.getFields("value") match {
case Seq(JsString(value)) ⇒ MyCaseClass
case _ ⇒ throw new DeserializationException(s"$json is not a valid case class")
}
Does that work for you?
Update:
Yes, your test is wrong. I tried it with the following (note parseJson instead of asJson) and it worked:
scala> val json = """{"value": "TEST"}""".parseJson
json: spray.json.JsValue = {"value":"TEST"}
scala> json.convertTo[MyCaseClass]
res2: MyCaseClass = MyCaseClass()
Update 2: Tried it with trait:
scala> import spray.json._
import spray.json._
scala> import spray.json.DefaultJsonProtocol._
import spray.json.DefaultJsonProtocol._
[Your code]
scala> val json = """{"value": "TEST"}""".parseJson
json: spray.json.JsValue = {"value":"TEST"}
scala> json.convertTo[MyTrait]
res1: MyTrait = MyCaseClass