Scala Play: How to map JSON array structure to Case Class - json

I'm completely new to Scala and Play and i stumbled upon the following the problem:
Given the following JSON structure:
[
{
"name": "Adam",
"age": 19
},
{
"name": "Berta",
"age": 22
},
...
]
I would like to map this JSON to a case classes like this:
case class User(name: String, age: Int)
case class Users(users: Seq[User])
or at least something like Seq[User].
I don't know how to traverse the JsPath because there is no key.
I tried to define an implicit read but either he cannot resolve the symbol "read" or he cannot found an implicit for user.
object User {
implicit val reads: Reads[User] = Json.reads[User]
}
object Users {
implicit val usersReads: Reads[Users] = (
(JsPath).read[Seq[User]]
)(Users.apply _)
}
How can I map my JSON to a working model?

Something like this will work
import play.api.libs.json._
case class User(name: String, age: Int)
case class Users(users: Seq[User])
object User {
implicit val reads = Json.reads[User]
}
object Users {
implicit val reads: Reads[Users] = Reads {
_.validate[Seq[User]].map(Users(_))
}
}

Related

How to convert a JSON string to an Object in KMM

Previously, I asked this question: Implementing generic method in interface that uses implementors class which allowed for an object to be converted to a JSON string.
But, now I would like to reverse the process. Ideally this would look like:
interface Domain {
constructor(json: String) {
/*...*/
}
}
#Serializable
class User(val a: Int, val b: Int): Domain {}
val user = User("{a: 3, b: 4}")
But, I'm unable to figure out how to construct an object directly from a JSON string.
A next best option would be create a static generator method:
interface Domain {
companion object {
inline fun <reified T> fromJSON(json: String): T {
return Json.decodeFromString(json)
}
}
}
val user = User.fromJSON("{a: 3, b: 4}")
But, this doesn't work at all because User does not inherit Domain's companion object. The 3rd best option:
val user = Domain.fromJSON<User>("{a: 3, b: 4}")
This does work from the Android side, however since fromJSON is declared inline and reified it is not exposed to iOS at all from the KMM.
Which brings me to my current solution:
#Serializable
class User(val a: Int, val b: Int): Domain {
companion object {
fun fromJSON(json: String): User { return Json.decodeFromString(json) }
}
}
val user = User.fromJSON("{a: 3, b: 4}")
This works, however it requires the above boilerplate code to be added to each and every 'Domain' object.
Is there anyway to improve on my current solution? (Of course, the higher up the chain the better.)
I think you have Object then you should need to convert it as :--
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.codewithfun.kotlin.jsonparser.models.Tutorial
fun main(args: Array<String>) {
val gson = Gson()
val gsonPretty = GsonBuilder().setPrettyPrinting().create()
val tutsList: List<Tutorial> = listOf(
Tutorial("Tut #1", "bezkoder", listOf("cat1", "cat2")),
Tutorial("Tut #2", "zkoder", listOf("cat3", "cat4"))
);
val jsonTutsList: String = gson.toJson(tutsList)
println(jsonTutsList)
val jsonTutsListPretty: String = gsonPretty.toJson(tutsList)
println(jsonTutsListPretty)
}
Then the output be like :--
[{"title":"Tut #1","author":"bezkoder","categories":["cat1","cat2"]},{"title":"Tut #2","author":"zkoder","categories":["cat3","cat4"]}]
[
{
"title": "Tut #1",
"author": "bezkoder",
"categories": [
"cat1",
"cat2"
]
},
{
"title": "Tut #2",
"author": "zkoder",
"categories": [
"cat3",
"cat4"
]
}
]

No instance of Reads is available for scala.collection.immutable.List in the implicit scope

I have nested case classes to deserialize a json object into object "C". THe json looks like this:
val jsonResp = {
"parent":{
"children": [
{"name": "a",
"house": "blue"}
{"name": "b",
"house": "green"}
]
}
}
I have nested class to deserialize the values.
case class Parent(children: Children)
case class Children(children: List[Child])
case class Child(name: String, house: String)
I am trying to get the "child" object at 0th index here:
val parent = jsonResp.as[Parent](Json.format[Parent])
val childrenRespObj = jsonResp.as[Children](Json.format[Children])
val child1 = Child(ChildrenRespObj.children.head.name, Child(ChildrenRespObj.children.head.house)
Encountered error: No instance of Reads is available for scala.collection.immutable.List in the implicit scope.
Since "Children" has List[Child] as a parameter then why its throwing the error? How do I resolve this? Thanks.
Json.format needs the dependency Format's to be able to create the Format for dependent type.
You need to make sure that the dependency Format's are available as implicit in the scope where the dependent Format is being created by Json.format[...].
import play.api.libs.json._
object CustomJsonImplicits {
implicit val childFormat = Json.format[Child]
implicit val childrenFormat = Json.format[Children]
implicit val parentFormat = Json.format[Parent]
}
import CustomJsonImplicits._
val parent = jsonResp.as[Parent]
val childAtZerothIndex = parent.children.children(0)
Edit
I just realised that your case classes are kind of wrong for this json. You have to keep in mind that the auto generated Format instances are very particular about json structure. Modifying for correctness.
import play.api.libs.json._
// it is good practice to define case classes as final
// but this code will work the same even without final
final case class Response(parent: Parent)
final case class Parent(children: List[Child])
final case class Child(name: String, house: String)
object CustomJsonImplicits {
implicit val childFormat = Json.format[Child]
implicit val parentFormat = Json.format[Parent]
implicit val responseFormat = Json.format[Response]
}
val jsonResp =
s"""|{
| "parent": {
| "children": [
| { "name": "a", "house": "blue" },
| { "name": "b", "house": "green" }
| ]
| }
|}""".stripMargin
import CustomJsonImplicits._
val jsValue = Json.parse(jsonResp)
val response = jsValue.as[Response]
println(response)
val childAtZerothIndex = response.parent.children(0)
println(childAtZerothIndex)
Also, you can try running this code as a Scastie snippet at - https://scastie.scala-lang.org/sarveshseri/dfBs6PvOT8KDTV4ITdTByA/9

spray json in scala: deserializing json with unknown fields without losing them

I'm looking for a solution to this but for json spray and my searches and attempts to get this working with json spray have failed thus far.
If I have the following json:
{
"personalData": {
"person": {
"first": "first_name",
"last": "last_name"
},
"contact": {
"phone": "1111111",
"address": {
"line": "123 Main St",
"city": "New York"
}
},
"xx": "yy", // unknown in advanced
"zz": { // unknown in advanced
"aa": "aa",
"bb": "bb",
"cc": {
"dd": "dd",
"ee": "ee"
}
}
}
}
We know for sure that the json will contain person and contact but we don't know what other fields may be created upstream from us that we don't care about/use.
I want to serialize this JSON into a case class containing person and contact but on the other hand, I don't want to lose the other fields (save them in a map so the class will be deserialized to the same json as received).
This is how far I've made it:
case class Address(
line: String,
city: String,
postalCode: Option[String]
)
case class Contact(
phone: String,
address: Address
)
case class Person(
first: String,
last: String
)
case class PersonalData(
person: Person,
contact: Contact,
extra: Map[String, JsValue]
)
implicit val personFormat = jsonFormat2(Person)
implicit val addressFormat = jsonFormat3(Address)
implicit val contactFormat = jsonFormat2(Contact)
implicit val personalDataFormat = new RootJsonFormat[PersonalData] {
def write(personalData: PersonalData): JsValue = {
JsObject(
"person" -> personalData.person.toJson,
"contact" -> personalData.contact.toJson,
// NOT SURE HOW TO REPRESENT extra input
)
}
def read(value: JsValue): CAERequestBEP = ???
}
Can someone help me do this with spray.json instead of play? I've spent such a long time trying to do this and can't seem to make it work.
In order to do that, you need to write your own formatter for PersonalDataFormat:
case class Person(first: String, last: String)
case class Address(line: String, city: String)
case class Contact(phone: String, address: Address)
case class PersonalData(person: Person, contact: Contact, extra: Map[String, JsValue])
case class Entity(personalData: PersonalData)
implicit val personFormat = jsonFormat2(Person)
implicit val addressFormat = jsonFormat2(Address)
implicit val contactFormat = jsonFormat2(Contact)
implicit object PersonalDataFormat extends RootJsonFormat[PersonalData] {
override def read(json: JsValue): PersonalData = {
val fields = json.asJsObject.fields
val person = fields.get("person").map(_.convertTo[Person]).getOrElse(???) // Do error handling instead of ???
val contact = fields.get("contact").map(_.convertTo[Contact]).getOrElse(???) // Do error handling instead of ???
PersonalData(person, contact, fields - "person" - "contact")
}
override def write(personalData: PersonalData): JsValue = {
JsObject(personalData.extra ++ ("person" -> personalData.person.toJson, "contact" -> personalData.contact.toJson))
}
}
implicit val entityFormat = jsonFormat1(Entity)
val jsonResult = jsonString.parseJson.convertTo[Entity]
The result is:
Entity(PersonalData(Person(first_name,last_name),Contact(1111111,Address(123 Main St,New York)),Map(xx -> "yy", zz -> {"aa":"aa","bb":"bb","cc":{}})))
(Assuming the json is not exactly the json above, but a valid similar one)
Code run in Scastie

JSON decode nested field as Map[String, String] in Scala using circe

A circe noob here. I am trying to decode a JSON string to case class in Scala using circe. I want one of the nested fields in the input JSON to be decoded as a Map[String, String] instead of creating a separate case class for it.
Sample code:
import io.circe.parser
import io.circe.generic.semiauto.deriveDecoder
case class Event(
action: String,
key: String,
attributes: Map[String, String],
session: String,
ts: Long
)
case class Parsed(
events: Seq[Event]
)
Decoder[Map[String, String]]
val jsonStr = """{
"events": [{
"ts": 1593474773,
"key": "abc",
"action": "hello",
"session": "def",
"attributes": {
"north_lat": -32.34375,
"south_lat": -33.75,
"west_long": -73.125,
"east_long": -70.3125
}
}]
}""".stripMargin
implicit val eventDecoder = deriveDecoder[Event]
implicit val payloadDecoder = deriveDecoder[Parsed]
val decodeResult = parser.decode[Parsed](jsonStr)
val res = decodeResult match {
case Right(staff) => staff
case Left(error) => error
}
I am ending up with a decoding error on attributes field as follows:
DecodingFailure(String, List(DownField(north_lat), DownField(attributes), DownArray, DownField(events)))
I found an interesting link here on how to decode JSON string to a map here: Convert Json to a Map[String, String]
But I'm having little luck as to how to go about it.
If someone can point me in the right direction or help me out on this that will be awesome.
Let's parse the error :
DecodingFailure(String, List(DownField(geotile_north_lat), DownField(attributes), DownArray, DownField(events)))
It means we should look in "events" for an array named "attributes", and in this a field named "geotile_north_lat". This final error is that this field couldn't be read as a String. And indeed, in the payload you provide, this field is not a String, it's a Double.
So your problem has nothing to do with Map decoding. Just use a Map[String, Double] and it should work.
So you can do something like this:
final case class Attribute(
key: String,
value: String
)
object Attribute {
implicit val attributesDecoder: Decoder[List[Attribute]] =
Decoder.instance { cursor =>
cursor
.value
.asObject
.toRight(
left = DecodingFailure(
message = "The attributes field was not an object",
ops = cursor.history
)
).map { obj =>
obj.toList.map {
case (key, value) =>
Attribute(key, value.toString)
}
}
}
}
final case class Event(
action: String,
key: String,
attributes: List[Attribute],
session: String,
ts: Long
)
object Event {
implicit val eventDecoder: Decoder[Event] = deriveDecoder
}
Which you can use like this:
val result = for {
json <- parser.parse(jsonStr).left.map(_.toString)
obj <- json.asObject.toRight(left = "The input json was not an object")
eventsRaw <- obj("events").toRight(left = "The input json did not have the events field")
events <- eventsRaw.as[List[Event]].left.map(_.toString)
} yield events
// result: Either[String, List[Event]] = Right(
// List(Event("hello", "abc", List(Attribute("north_lat", "-32.34375"), Attribute("south_lat", "-33.75"), Attribute("west_long", "-73.125"), Attribute("east_long", "-70.3125")), "def", 1593474773L))
// )
You can customize the Attribute class and its Decoder, so their values are Doubles or Jsons.

Play framework read from json list

So I have a structure in json looking like this:
{
"lst": [
{"name": "foo"},
{"name": "bar"}
]
}
I'm having the hardest time converting this to a list of case classes. I'm sure I'm missing something completely obvious...
I have tried this:
case class Person(name: String)
implicit val personReads: Reads[Email] = (__ \\ "name").read[String](Person)
// endpoint
def person = Action { request =>
val person = request.body.asJson.get.as[Seq[Person]]
}
which doesn't compile since read doesn't return a FunctionBuilder which means I can't apply the path to Person
Adding a new parameter does compile (changing the json and case class accordingly):
case class Person(name: String, age: String)
implicit val personReads: Reads[Email] = (
(__ \\ "name").read[String]) and
(__ \\ "age").read[Int](Person)
but throws an exception Execution exception[[JsResultException: JsResultException(errors:List((,List(ValidationError(List(error.expected.jsarray),WrappedArray())))))]] supposedly because it expects a list.
So I tried adding this:
implicit val personsReads: Reads[Seq[Person]] = (__ \ "lst").read[Seq[Person]]
which then throws a NullPointer.
In the end I just want a Seq[Person].
Can anyone point me in the right direction, I'm completely lost to what I'm expected to do here...
You can do the following instead of giving reads and writes explicitly.
import play.api.json.Json
case class Person(name: String)
object Person {
implicit val personFormat = Json.format[Person]
}
case class Persons(lst: List[Person])
object Persons {
implicit val personsFormat = Json.format[Persons]
}
Now take the json string lets say jsonStr
Json.parse(jsonStr).validate[Persons] match {
case JsSuccess(persons, _) => println(persons)
case JsError(_) => println("parsing failed")
}
Scala REPL
scala> import play.api.libs.json._
import play.api.libs.json._
scala> val str = """{
| "lst": [
| {"name": "foo"},
| {"name": "bar"}
| ]
| }""".stripMargin
str: String =
{
"lst": [
{"name": "foo"},
{"name": "bar"}
]
}
scala> :paste
// Entering paste mode (ctrl-D to finish)
case class Person(name: String)
object Person {
implicit val personFormat = Json.format[Person]
}
case class Persons(lst: List[Person])
object Persons {
implicit val personsFormat = Json.format[Persons]
}
// Exiting paste mode, now interpreting.
defined class Person
defined object Person
defined class Persons
defined object Persons
scala> val jsonStr = str
jsonStr: String =
{
"lst": [
{"name": "foo"},
{"name": "bar"}
]
}
scala> :paste
// Entering paste mode (ctrl-D to finish)
Json.parse(jsonStr).validate[Persons] match {
case JsSuccess(persons, _) => println(persons)
case JsError(_) => println("parsing failed")
}
// Exiting paste mode, now interpreting.
Persons(List(Person(foo), Person(bar)))
Now when you change your Person case class and add age field.
case class Person(name: String, age: Int)
object Person {
implicit val personFormat = Json.format[Person]
}
Ensure that the json you are trying to parse contains both name and age. If you have only name then you will get parsing error.