I am currently using the Play framework json parser in order to parse a json string in my scala code.
I have the following class:
case class Address(address: String,
gps: GPS,
country: String) {}
object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[GPS] and
(JsPath \ "country").read[String]
) (Address.apply _)
implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").write[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}
Which works fine with the following json:
{
"address": "123 Fake Street",
"country": "USA",
"gps": { ... }
}
The problem is that in some situations the json may instead have the gps field be a string which doesnt parse, i.e.
{
"address": "123 Fake Street",
"country": "USA",
"gps": "123abc"
}
Now I know that I cant have the gps member be both a string or a GPS object, but is there any way to have it be say an Option[GPS] and only have a value if the json contained a gps object?
Only very little is needed to be changed in your impl.
You need to read the field "gps" as something that is 'safe' like JsValue and then try to map it into your GPS case class if it can be done, if not, return None.
case class GPS(a:String, b:String)
object GPS {
val travelInfoReads = Json.reads[GPS]
val travelInfoWrites = Json.writes[GPS]
implicit val travelInfoFormat: Format[GPS] = Format(travelInfoReads, travelInfoWrites)
}
case class Address(address: String,
gps: Option[GPS],
country: String) {}
object Address {
implicit val reads: Reads[Address] = (
(JsPath \ "address").read[String] and
(JsPath \ "gps").read[JsValue].map(js => js.asOpt[GPS]) and
(JsPath \ "country").read[String]
) (Address.apply _)
implicit val writes: Writes[Address] = (
(JsPath \ "address").write[String] and
(JsPath \ "gps").writeNullable[GPS] and
(JsPath \ "country").write[String]
) (unlift(Address.unapply))
}
I also tested it:
val json = Json.toJson(Address("1",Some(GPS("a","b")),"2"))
println(json)
println(json.as[Address])
val newObj: JsObject = (json.as[JsObject] - "gps") + ("gps" -> JsNumber(1))
println(newObj)
val a = newObj.as[Address]
println(a)
a must beEqualTo(Address("1",None,"2"))
Output was like
{"address":"1","gps":{"a":"a","b":"b"},"country":"2"}
Address(1,Some(GPS(a,b)),2)
{"address":"1","country":"2","gps":1}
Address(1,None,2)
Related
consider i have a json as following:
{
"a": "aa",
"b": "bb",
"c": "cc",
"d": "dd", // unknown in advance
"e": { //unknown in advance
"aa": "aa"
}
}
i know for sure that the json will contain a,b,c but i've no idea what other fields this json may contain.
i want to serialize this JSON into a case class containing a,b,c but on the other hand not to lose the other fields (save them in a map so the class will be deserialized to the same json as received).
ideas?
One option is to capture the "unknown" fields in a Map[String,JsValue], from which you can later extract values if you need them.
case class MyClass(a: String, b: String, c: String, extra: Map[String, JsValue])
implicit val reads: Reads[MyClass] = (
(__ \ "a").read[String] and
(__ \ "b").read[String] and
(__ \ "c").read[String] and
__.read[Map[String, JsValue]]
.map(_.filterKeys(k => !Seq("a", "b", "c").contains(k)))
)(MyClass.apply _)
// Result:
// MyClass(aa,bb,cc,Map(e -> {"aa":"aa"}, d -> "dd"))
Likewise, you can do a Writes or a Format like so:
// And a writes...
implicit val writes: Writes[MyClass] = (
(__ \ "a").write[String] and
(__ \ "b").write[String] and
(__ \ "c").write[String] and
__.write[Map[String, JsValue]]
)(unlift(MyClass.unapply _))
// Or combine the two...
implicit val format: Format[MyClass] = (
(__ \ "a").format[String] and
(__ \ "b").format[String] and
(__ \ "c").format[String] and
__.format[Map[String, JsValue]](Reads
.map[JsValue].map(_.filterKeys(k => !Seq("a", "b", "c").contains(k))))
)(MyClass.apply, unlift(MyClass.unapply))
Note: it looks a bit confusing because you give the format for Map[String,JsValue] an explicit Reads as an argument (Reads.map), which you then transform (using the .map method) to remove the already-captures values.
You can use a custom Reads for this, something like:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class MyData(a: String, b: String, c:String, other: Map[String, JsValue])
object MyData {
val abcReader: Reads[(String, String, String)] = (
(JsPath \ "a").read[String] and
(JsPath \ "b").read[String] and
(JsPath \ "c").read[String]
).tupled
implicit val reader: Reads[MyData] = Reads { json =>
abcReader.reads(json).map {
case (a, b, c) =>
val other = json.as[JsObject].value -- Seq("a", "b", "c")
MyData(a, b, c, other.toMap)
}
}
}
I am trying to figure out the best and elegant way to tim values on an in coming json.
So for example I have the following json:
{
"firstName": " foo",
"lastName": "bar "
}
With the following definitions:
case class Someone(firstName:String, lastName: String)
object Someone{
implicit val someoneReads: Reads[Someone] = (
(JsPath \ "firstName").read[String] and
(JsPath \ "lastName").read[String]
)(Someone.apply _)
}
Is there a way to trim the json while reading it? or I need to write a transformer for that? and if I do, how to write it so it will be generic to trip every json I will provide?
Thanks!
Use map(_.trim) for read[String] for trim string (universal solution)
implicit val someoneReads: Reads[Someone] = (
(JsPath \ "firstName").read[String].map(_.trim) and
(JsPath \ "lastName").read[String].map(_.trim)
)(Someone.apply _)
You can implement own Reads[String] with trimmed string also
def trimmedString(path: JsPath): Reads[String] = Reads.at[String](path).map(_.trim)
implicit val someoneReads: Reads[Someone] = (
trimmedString(JsPath \ "firstName") and trimmedString(JsPath \ "lastName")
)(Someone.apply _)
For a more familiar view of code you may implement implicit conversion
import scala.language.implicitConversions
class JsPathHelper(val path: JsPath) {
def trimmedString: Reads[String] = Reads.at[String](path).map(_.trim)
}
implicit def toJsPathHelper(path: JsPath): JsPathHelper = new JsPathHelper(path)
implicit val someoneReads: Reads[Someone] = (
(JsPath \ "firstName").trimmedString and
(JsPath \ "lastName").trimmedString
)(Someone.apply _)
You can specify your own reads[String] based on the default one, and then use macros:
object Someone {
implicit val trimStringReads: Reads[String] = Reads.StringReads.map(_.trim)
implicit val someoneReads: Reads[Someone] = Json.reads[Someone]
}
I am trying to update some code. I am having problem with this case class trying to write the json implicit writer
case class TemplateEmailMessage(recipients: List[EmailRecipient], globalEmailVars: List[(String, String)])
It was like this
implicit val templateEmailMessageWrites = new Writes[TemplateEmailMessage] {
def writes(m: TemplateEmailMessage): JsValue = {
val globalVars: List[JsValue] = m.globalEmailVars.map(g => Json.obj("name" -> g._1, "content" ->g._2))
Json.obj(
"to" -> m.recipients,
"global_merge_vars" -> JsArray(globalVars)
)
}
}
Now is like this. Obviously is not working because the type of the second field List[(String, String)]
object TemplateEmailMessage {
implicit val templateEmailMessageWrites: Writes[TemplateEmailMessage] = (
(JsPath \ "to").write[List[EmailRecipient]] and
(JsPath \ "global_merge_vars").write[List[(String, String)]]
)(unlift(TemplateEmailMessage.unapply))
}
I am not sure how to translate the JsArray, because before it was use to manipulate the values in the writer. I should leave like the old way or there is another way?
Thank you
You can do it like this:
object TemplateEmailMessage{
implicit val templateEmailMessageWrites: Writes[TemplateEmailMessage] = (
(JsPath \ "to").write[List[EmailRecipient]] and
(JsPath \ "global_merge_vars").write[JsArray]
.contramap[List[(String, String)]](list => JsArray(list.map(g => Json.obj("name" -> g._1, "content" ->g._2))))
)(unlift(TemplateEmailMessage.unapply))
}
Other option, is to convert the (String, String) tuple to a case class and create a writer for it.
case class Variable(name: String, content: String)
object Variable{
implicit val variableWrites: Writes[Variable] = (
(JsPath \ "name").write[String] and
(JsPath \ "content").write[String]
)(unlift(Variable.unapply))
}
And then:
implicit val templateEmailMessageWrites: Writes[TemplateEmailMessage] = (
(JsPath \ "to").write[List[EmailRecipient]] and
(JsPath \ "global_merge_vars").write[List[Variable]]
)(unlift(TemplateEmailMessage.unapply))
I've been working with some legacy code and I've some troubles.
I have enum written by other guy and now I should write into database some data and read from it. Here some examples:
Here's the enum
object MarketType extends scala.Enumeration {
type MarketType = Value
val Continent, Country, State, City = Value
implicit val marketTypeReads = EnumUtility.enumFormat(MarketType)
}
case class Market(
id: ObjectId = new ObjectId,
name: String,
marketType: Option[MarketType] = None,
)
object Market {
val writes = new Writes[Market] {
def writes(o: Market) = Json.obj(
"id" -> o.id,
"name" -> o.name,
"type" -> o.marketType
)
}
val reads: Reads[Market] = (
(JsPath \ "id").readNullable[ObjectId].map(validateAndFetchObjectId) and
(JsPath \ "name").read[String] and
(JsPath \ "type").read[MarketType]
)(Market.apply _)
implicit val format: Format[Market] = Format(reads, writes)
Here's JSON which I take from front-end and what I should write to the database :
{"name":"Name","type":"state","description":"Desc"}
Change this:
val reads: Reads[Market] = (
(JsPath \ "id").readNullable[ObjectId].map(validateAndFetchObjectId) and
(JsPath \ "name").read[String] and
(JsPath \ "type").read[MarketType]
)(Market.apply _)
Into this:
val reads: Reads[Market] = (
(JsPath \ "id").readNullable[ObjectId].map(validateAndFetchObjectId) and
(JsPath \ "name").read[String] and
(JsPath \ "type").read[MarketType]
)((id, name, type) => Market(id, name, Some(MarketType.withName(type))))
Things to watch out for:
The JSON you posted has the value of the type field in lower case; this will throw a NoSuchElement exception in production when passed into the MarketType.withName() method. Ensure the clients of your API send this field in Camel case
Example: { .., type: "State", .. }
I cannot find a way how to make it work when deserialized object has single field - I cannot compile the code. Seems that and operator does some transformation and I cannot find a method to call to do the same.
I have following json:
{"total": 53, "max_score": 3.2948244, "hits": [
{
"_index": "h",
"_type": "B",
"_id": "3413569628",
"_score": 3.2948244,
"_source": {
"fotky": [
{
"popisek":" ",
"localFileSystemLocation":" ",
"isMain": true,
"originalLocation": ""
}
]
}
}
]
}
I try the following data model to de serialize to:
case class SearchLikeThisResult(total: Int, max_score: Double, hits: Seq[Hits])
case class Hits(_index: String, _type: String, _id: String, _score: Double, _source: Source)
case class Source(fotky: Seq[Photo])
case class Photo(isMain: Boolean, originalLocation: Option[String], localFileSystemLocation: Option[String], popisek: Option[String])
Implicit reads as follows:
object SearchLikeThisHits {
import play.api.libs.functional.syntax._
implicit val photoReads: Reads[Photo] = (
(JsPath \ "isMain").read[Boolean] and
(JsPath \ "originalLocation").readNullable[String] and
(JsPath \ "localFileSystemLocation").readNullable[String] and
(JsPath \ "popisek").readNullable[String]
)(Photo.apply _)
implicit val sourceReads: Reads[Source] = (
(JsPath \ "fotky").read[Seq[Photo]]
)(Source.apply _)
implicit val hitsReads: Reads[Hits] = (
(JsPath \ "_index").read[String] and
(JsPath \ "_type").read[String] and
(JsPath \ "_id").read[String] and
(JsPath \ "_score").read[Double] and
(JsPath \ "_source").read[Source]
)(Hits.apply _)
implicit val searchLikeThisResult: Reads[SearchLikeThisResult] = (
(JsPath \ "total").read[Int] and
(JsPath \ "max_score").read[Double] and
(JsPath \ "hits").read[Seq[Hits]]
)(SearchLikeThisResult.apply _)
}
What I am really struggling with is under the _source:
implicit val sourceReads: Reads[Source] = (
(JsPath \ "fotky").read[Seq[Photo]]
)(Source.apply _)
where read is reported as unkown symbol - in other cases and performs some transformation.
Inline definition doesn't help either.
Does anybody faced this before?
The fancy applicative builder syntax (and, etc.) is nice, but it can obscure the fact that Reads is monadic and also works perfectly well with map, flatMap, for-comprehensions, etc.
So while the applicative builder syntax doesn't work with single values, plain old map does:
implicit val sourceReads: Reads[Source] =
(JsPath \ "fotky").read[Seq[Photo]].map(Source(_))
The key here is that (JsPath \ "fotky").read[Seq[Photo]] is a Reads[Seq[Photo]], and you want a Reads[Source]. map gives you a way to get from one to the other, just as you could use it to transform an Option[Seq[Photo]] into an Option[Source, for example.
You could save yourself some trouble by making use of the Json.reads to automatically generate your Reads (provided the case class is defined exactly like the Json objects - which is your case).
implicit val photoReads = Json.reads[Photo]
implicit val sourceReads = Json.reads[Source]
implicit val hitsReads = Json.reads[Hits]
implicit val searchResultReads = Json.reads[SearchLikeThisResult]
For more information, see https://www.playframework.com/documentation/2.1.1/ScalaJsonInception