HowTo skip deserialization for a field in json4s - json

Here is my json:
{
"stringField" : "whatever",
"nestedObject": { "someProperty": "someValue"}
}
I want to map it to
case class MyClass(stringField: String, nestedObject:String)
nestedObject should not be deserialized, I want json4s to leave it as string.
resulting instance shouldBe:
val instance = MyClass(stringField="whatever", nestedObject= """ { "someProperty": "someValue"} """)
Don't understand how to do it in json4s.

You can define a custom serializer:
case object MyClassSerializer extends CustomSerializer[MyClass](f => ( {
case jsonObj =>
implicit val format = org.json4s.DefaultFormats
val stringField = (jsonObj \ "stringField").extract[String]
val nestedObject = compact(render(jsonObj \ "nestedObject"))
MyClass(stringField, nestedObject)
}, {
case myClass: MyClass =>
("stringField" -> myClass.stringField) ~
("nestedObject" -> myClass.nestedObject)
}
))
Then add it to the default formatter:
implicit val format = org.json4s.DefaultFormats + MyClassSerializer
println(parse(jsonString).extract[MyClass])
will output:
MyClass(whatever,{"someProperty":"someValue"})
Code run at Scastie

Related

validate json with type value

I wand to validate json that has a type value
I have a case class SearchRequestMessage with a companion object with json writer
Value T can be Long or UUID
case class SearchRequestMessage[T](jobId: UUID, offerId: UUID, search: T, searchByType: SearchByType)
object SearchRequestMessage{
implicit def searchResultsWrites[T: Writes]: Writes[SearchRequestMessage[T]] = new Writes[SearchRequestMessage[T]] {
def writes(searchSesult: SearchRequestMessage[T]) =
JsObject(
Seq(
"jobId" -> JsString(searchSesult.jobId.toString),
"offerId" -> JsString(searchSesult.offerId.toString),
"search" -> JsString(searchSesult.search.toString),
"searchByType" -> JsString(searchSesult.searchByType.toString)
))
}
}
Also I have SearchByTypes enum
object SearchByTypes extends Enumeration {
type SearchByType = Value
val Emails: SearchByType = Value(0, "EMAILS")
val Offer: SearchByType = Value(1, "OFFER")
implicit val SearchByTypeFormat: Format[SearchByTypes.Value] = JsonFormatter.enumFormat(SearchByTypes)
}
enum formatter
import play.api.libs.json._
object JsonFormatter {
def enumFormat[T <: Enumeration](enum: T): Format[T#Value] = new EnumFormatter[T](enum)
def enumWithIdsFormat[T <: Enumeration](enum: T): Format[T#Value] = new EnumWithIdsFormatter[T](enum)
class EnumFormatter[T <: Enumeration](enum: T) extends Format[T#Value] {
override def writes(o: T#Value): JsValue = o match {
case null => JsNull
case _ => JsString(o.toString)
}
override def reads(json: JsValue): JsResult[T#Value] = json match {
case JsString(value) => {
try {
JsSuccess(enum.withName(value))
} catch {
case _: NoSuchElementException =>
JsError(
s"Enumeration expected of type: '${enum.getClass}', but it does not appear to contain the value: '$value'")
}
}
case _ => JsError(s"Invalid JSON: $json. Error in '${enum.getClass}' field. Possible values: ${enum.values}")
}
}
class EnumWithIdsFormatter[T <: Enumeration](enum: T) extends Format[T#Value] {
private val nameField = "name"
private val idField = "id"
override def writes(o: T#Value): JsValue = JsObject(Map(idField -> JsNumber(o.id), nameField -> JsString(o.toString)))
override def reads(json: JsValue): JsResult[T#Value] = json match {
case JsObject(values) =>
values.get(idField) match {
case Some(JsNumber(value)) if value <= enum.maxId && value >= 0 ⇒
try {
JsSuccess(enum(value.toInt))
} catch {
case _: NoSuchElementException =>
JsError(
s"Enumeration expected of type: '${enum.getClass}', but it does not appear to contain the value: '$value'")
}
case Some(JsNumber(x)) ⇒
JsError(s"Invalid JSON:$json. Field '$idField': '$x' is out of range. Possible values:${enum.values.map(_.id)}")
case Some(_) ⇒
JsError(s"Invalid JSON:$json. Field '$idField' isn't number. Possible values:${enum.values.map(_.id)}")
case None ⇒
JsError(s"Invalid JSON:$json. Missing field '$idField'")
}
case _ =>
JsError(s"Invalid JSON: $json")
}
}
}
Now I want to validate json
import play.api.libs.json.Json
import play.api.libs.functional.syntax._
val obj = SearchRequestMessage(UUID.randomUUID(), UUID.randomUUID(), "email", SearchByTypes.Emails)
val json = Json.toJson(obj)
val result = ((json \ "jobId").validate[UUID] and
(json \ "offerId").validate[UUID] and
(json \ "search").validate[String] and
(json \ "searchByType").validate[SearchByType])(SearchRequestMessage[String].apply(_,_,_,_))
and an error appear:
Error:(41, 75) missing argument list for method apply in object SearchRequestMessage
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `apply _` or `apply(_,_,_,_)` instead of `apply`.
(json \ "searchByType").validate[SearchByType])(SearchRequestMessage[String].apply(_,_,_,_))
How I can fix this error?
try
(json \ "searchByType").validate[SearchByType])(SearchRequestMessage[String].apply)
or (which is a bit verbose)
(json \ "searchByType").validate[SearchByType])((a,b,c,d) => SearchRequestMessage[String].apply(a,b,c,d))
This works
((json \ "jobId").validate[UUID] and
(json \ "offerId").validate[UUID] and
(json \ "search").validate[String] and
(json \ "searchByType").validate[SearchByType])((a: UUID, b: UUID, c: String, d: SearchByType) => SearchRequestMessage.apply[String](a,b,c,d))

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])

convert json to array of scala objects using spray json

I am not much familier with spray json, but I have to convert the below json into Array[myTest]
Below is the code, but it doesnt work. It throws the following errors: How do I fix them?
Error:(19, 54) Cannot find JsonReader or JsonFormat type class for Array[A$A61.this.myTest]
lazy val converted= trainingDataRef.toJson.convertTo[Array[myTest]]
^
Error:(19, 54) not enough arguments for method convertTo: (implicit evidence$1: spray.json.JsonReader[Array[A$A61.this.myTest]])Array[A$A61.this.myTest].
Unspecified value parameter evidence$1.
lazy val converted= trainingDataRef.toJson.convertTo[Array[myTest]]
^
Error:(10, 61) could not find implicit value for evidence parameter of type spray.json.DefaultJsonProtocol.JF[Map[String,Any]]
implicit val format: RootJsonFormat[myTest] = jsonFormat3(myTest.apply)
^
Code: ^
import spray.json.DefaultJsonProtocol._
import spray.json._
case class myTest (
id: String,
classDetails: Map[String, Any],
school: Map[String, Any])
object myTest {
implicit val format: RootJsonFormat[myTest] = jsonFormat3(myTest.apply)
}
val trainingDataRef = """[{"id":"my-id","classDetails":{"sec":"2","teacher":"John"},"school":{"name":"newschool"}}]"""
println(trainingDataRef.getClass)
val converted= trainingDataRef.toJson.convertTo[Array[myTest]]
println(converted)
spray-json has good documentation, try take a look there. Basically, you have to define your case classes and implement JsonFormat for them:
import spray.json.DefaultJsonProtocol._
import spray.json._
case class ClassDetails(sec: String, teacher: String)
object ClassDetails {
implicit val format: RootJsonFormat[ClassDetails] = jsonFormat2(ClassDetails.apply)
}
case class School(name: String)
object School {
implicit val format: RootJsonFormat[School] = jsonFormat1(School.apply)
}
case class ClassInfo
(
id: String,
classDetails: ClassDetails,
school: School
)
object ClassInfo {
implicit object ClassInfoFormat extends RootJsonFormat[ClassInfo] {
def write(c: ClassInfo): JsValue = JsObject(
"id" -> JsString(c.id),
"classDetails" -> c.classDetails.toJson,
"school" -> c.school.toJson
)
def read(value: JsValue): ClassInfo = {
value.asJsObject.getFields("id", "classDetails", "school") match {
case Seq(JsString(name), details, school) =>
new ClassInfo(name, details.convertTo[ClassDetails], school.convertTo[School])
case _ => throw new DeserializationException("ClassInfo expected")
}
}
}
}
val json = """[{"id":"my-id","classDetails":{"sec":"2","teacher":"John"},"school":{"name":"newschool"}}]"""
// JSON string to case classes
val classInfos = json.parseJson.convertTo[Seq[ClassInfo]]
classInfos.zipWithIndex.foreach { case (c, idx) =>
println(s"$idx => $c")
}
println
// Seq[ClassInfo] to JSON
println(s"$classInfos: ")
println(classInfos.toJson.prettyPrint)

How to edit existing JSON object with sprayJSON

I am using akka with spray json support for which I need to edit value in the recieved json.
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json._
final case class Item(name: String, id: Long)
final case class Order(items: List[Item],orderTag:String)
trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val itemFormat = jsonFormat2(Item)
implicit val orderFormat = jsonFormat2(Order)
}
In my use case I recieve the json with orderTag value as null, all I need to do is edit the orderTag value with and then use it as entity value.Is it possible to write/edit jsonObject and How to do that ?
class MyJsonService extends Directives with JsonSupport {
// format: OFF
val route =
get {
pathSingleSlash {
complete(Item("thing", 42)) // will render as JSON
}
} ~
post {
entity(as[Order]) { order => // will unmarshal JSON to Order
val itemsCount = order.items.size
val itemNames = order.items.map(_.name).mkString(", ")
complete(s"Ordered $itemsCount items: $itemNames")
}
}
}
You can just edit the json AST like ..
val json = """{"orderTag":null}"""
val jsVal = json.parseJson
val updatedJs = if (jsObj.fields.get("orderTag") == Some(JsNull)) {
JsObject(jsObj.fields + ("orderTag" -> JsString("new tag")))
} else {
jsObj
}
updatedJs.compactPrint
res26: String = """
{"orderTag":"new tag"}
"""

Implicit parameters not found

I'm having some trouble figuring out why compiler complains about not finding an implicit parameter for reads because I'm almost sure that it is in the scope. The error is the following:
Error:(13, 18) No Json deserializer found for type Config. Try to implement an implicit Reads or Format for this type.
test.validate[Config].map {
^
Error:(13, 18) not enough arguments for method validate: (implicit rds: play.api.libs.json.Reads[Config])play.api.libs.json.JsResult[wings.m2m.conf.model.Config].
Unspecified value parameter rds.
test.validate[Config].map {
^
and it happens in the following code:
import play.api.libs.json._
import play.api.libs.json.Reads._
import Config.JsonImplicits._
import scala.util.Try
object Test {
def main(args: Array[String]) {
val test = Json.obj("action" -> Config.Action.nameAcquisitionRequest.toString, "value" -> "hola")
test.validate[Config].map {
t => println(t)
t
}
}
}
/**
* Config companion object
*/
object Config {
type ValueType = String
val ActionKey = "action"
val ValueKey = "value"
object Action extends Enumeration {
type Action = Value
val nameAcquisitionRequest = Value("nameAcquisitionRequest")
val nameAcquisitionReject = Value("nameAcquisitionReject")
val nameAcquisitionAck = Value("nameAcquisitionAck")
val broadcast = Value("broadcast")
}
/**
* Json implicit conversions
*/
object JsonImplicits {
implicit object ConfigReads extends Reads[Config] {
def hTypeCast(action: Config.Action.Value, value: Config.ValueType): Config = {
action match {
case Config.Action.nameAcquisitionRequest => NameAcquisitionRequest(value)
case Config.Action.nameAcquisitionReject => NameAcquisitionReject(value)
case Config.Action.nameAcquisitionAck => NameAcquisitionAck(value)
}
}
override def reads(json: JsValue): JsResult[Config] = json match {
case json: JsObject =>
val action = (json \ ActionKey).as[String]
Try(Config.Action.withName(action)) map {
a =>
val value = (json \ ValueKey).as[String]
JsSuccess(hTypeCast(a, value))
} getOrElse (JsError("Can't convert to Config"))
case _ => JsError("Can't convert to Config")
}
}
implicit object ConfigWrites extends OWrites[Config] {
def jsObjectCreator(action: Config.Action.Value, value: Config.ValueType): JsObject = {
Json.obj(ActionKey -> action.toString, ValueKey -> Json.toJson(value))
}
override def writes(o: Config): JsObject = o match {
case c: NameAcquisitionRequest => jsObjectCreator(Config.Action.nameAcquisitionRequest, c.value)
case c: NameAcquisitionReject => jsObjectCreator(Config.Action.nameAcquisitionReject, c.value)
case c: NameAcquisitionAck => jsObjectCreator(Config.Action.nameAcquisitionAck, c.value)
}
}
}
}
sealed trait Config {
val value: Config.ValueType
}
/**
* Intermediate config message
* #param value
*/
case class NameAcquisitionRequest(override val value: String)
extends Config
case class NameAcquisitionReject(override val value: String)
extends Config
case class NameAcquisitionAck(override val value: String)
extends Config
case class Broadcast(override val value: String)
extends Config
the error occurs when executing the main method on the Test object. To make this example work, make sure to add the following dependency in the SBT: "com.typesafe.play" %% "play-json" % "2.4.1" . And I'm not sure, but maybe this resolver is needed: resolvers += "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases/"
I am not sure what you are trying to achieve and whether this solves your problem but here you go:
test.validate[Config](Config.JsonImplicits.ConfigReads).map {
t => println(t)
t
}