I have the following use-case:
Each class that I'm serde using JSON4S have a field, named ID. This ID can be any type T <: Stringifiable, where Stringifiable requires your ID type to be hashed to a string. Stringifiables also have constructors that rebuilds them from a string.
I'd like to serde any Stringifiable, for example ComplexIdentifier to a JSON of ID: stringified_identifier. Serialization works nicely, but unfortunately during deserialization, JSON4S is not going to use the default constructor which has only 1 string constructor. It finds the constructor, but if the identifier has a signature of case class ComplexIdentifier(whatever: String), it tries to extract a whatever name from the JString(stringified_identifier). That fails, so MappingException is thrown internally.
Is there any way to teach JSON4S to use the default constructor without extracting the values like this? It would be so obvious to just use the value from the JString and construct the Stringifiable using that.
Thanks!
Use the applymethod in a Companion to overload the constructor for the ID classes with a String parameter. Then just use a custom serializer for all of your ID types
sealed abstract class Stringifiable {}
case class ComplexIdentifier(whatever: List[Long]) extends Stringifiable
case class SimpleIdentifier(whatever: Int) extends Stringifiable
//Overload the default constructor
object ComplexIdentifier {
def apply(s: String):ComplexIdentifier = {
ComplexIdentifier(s.split(",").map(_.toLong).toList)
}
}
case class MyClass(id: ComplexIdentifier, value: String)
Then use a custom serializer:
case object ComplexIdentifierSerializer extends CustomSerializer[ComplexIdentifier] ( formats =>
({
case JString(id) => ComplexIdentifier(id)
case JNull => null
},
{
case x: ComplexIdentifier => JString(x.whatever.mkString(","))
}))
Finally, make sure to include the serializer in the implicit formats:
implicit val formats = DefaultFormats ++ List(ComplexIdentifierSerializer)
println(parse("""
{
"id": "1",
"value": "big value"
}
""").extract[MyClass])
val c = MyClass(ComplexIdentifier("123,456"), "super value")
println(write(c))
Related
How to configure the spray-json parsing on parsing options?
Similarly as Jackson Parsing Features.
For example, I am parsing a json that has a field that my case class has not, and it is breaking:
spray.json.DeserializationException: Object is missing required member 'myfield'
UPDATE :
A simple example:
case class MyClass(a: String, b: Long);
and try to parse an incomplete json like
val data = "{a: \"hi\"}"
with a spray-json format like:
jsonFormat2(MyClass.apply)
// ...
data.parseJson.convertTo[MyClass]
(simplified code).
But the question goes further, I want to ask about configuration options like in other parsers. More examples:
Be able to ignore fields that exist in the JSON but not in the case class.
Ways of managing nulls or nonexistent values.
etc.
SprayJson allows you to define custom parsers like so:
case class Foo(a: String, b: Int)
implicit object FooJsonFormat extends RootJsonFormat[Foo] {
override def read(json: JsValue): Foo = {
json.asJsObject.getFields("name", "id") match {
case Seq(JsString(name), id) =>
Foo(name, id.convertTo[Int])
}
}
override def write(obj: Foo): JsValue = obj.toJson
}
This allows you to parse any arbitrary payload and pull out the fields "name" and "id" - other fields are ignored. If those fields are not guaranteed you can add something like:
case Seq(JsString(name), JsNull) =>
Foo(name, 0)
You should look at what's available in JsValue.scala - in particular JsArray may come in handy if you're getting payloads with anonymous arrays (i.e. the root is [{...}] instead of {"field":"value"...})
Spray Json doesn't support default parameters. So You cannot have a case class like
case class MyClass(a: String, b: Int = 0)
and then parse json like {"a":"foo"}
However if you make the second parameter as Option. then it works.
import spray.json._
case class MyClass(a: String, b: Option[Int] = None)
object MyProtocol extends DefaultJsonProtocol {
implicit val f = jsonFormat2(MyClass)
}
import MyProtocol.f
val mc1 = MyClass("foo", Some(10))
val strJson = mc1.toJson.toString
val strJson2 = """{"a": "foo"}"""
val mc2 = strJson2.parseJson.convertTo[MyClass]
println(mc2)
Let's say I have the next case class:
case class Person(id: String, money: BigDecimal)
object Person {
implicit val encoder: Encoder[Person] = Encoder.forProduct2("ID", "Money")(u =>
(u.id, u.money))
I want to serialize instances of the Person class to JSON, so when I evaluate the asJson from circe, I get the result in scientific notation:
{
"ID" : "123",
"VALOR_SAP" : 2.7E+7
}
Why do this happens? I think the reason is because the default to string of BigDecimal automatically format to scientific notation.
What could I do to avoid this? May be creating another type which extends from BigDecimal and overriding the toString?
I assume that you use scala.math.BigDecimal, for java.math.BigDecimal code is similar. The way to change how objects are serialized is to provide corresponding implicit Encoder object. Unfortunately both Json and JsonNumber hierachies are sealed, so there is no very clean solution but you still can use JsonNumber.fromDecimalStringUnsafe that implements toString to just return any string you passed in. So you can do something like this:
case class Person(id: String, money: BigDecimal)
object Person {
implicit final val bigDecimalAsPlainStringEncoder: Encoder[BigDecimal] = new Encoder[BigDecimal] {
final def apply(value: BigDecimal): Json = Json.fromJsonNumber(JsonNumber.fromDecimalStringUnsafe(value.bigDecimal.toPlainString))
}
implicit val encoder: Encoder[Person] = Encoder.forProduct2("ID", "Money")(u => (u.id, u.money))
}
Here is spray-json example. Here is NullOptions trait.
The problem is when I declare a case class say
object MyJsonProtocol extends DefaultJsonProtocol {
implicit val some: RootJsonFormat[Some] = jsonFormat2(Some)
}
case class Some (
name:String,
age:Int
)
and json do not contains a field for example:
{
"name":"John"
}
I get: java.util.NoSuchElementException: key not found: age
So I have to add an Option and NullOption trait like that:
object MyJsonProtocol extends DefaultJsonProtocol with NullOptions {
implicit val some: RootJsonFormat[Some] = jsonFormat2(Some)
}
case class Some (
name:String,
age:Option[Int]
)
Everything works. But I do not want to have a case classes where all member are Option. Is there a way to configure spray json unmarshalling to just set nulls without additional Option type?
P.S.
I understand that in general Option is better then null check, but in my case it is just monkey code.
Also complete example of marshalling during response processing is here
The only way I can think of is to implement your own Protocol via read/write, which might be cumbersome. Below is a simplified example. Note that I changed the age to be an Integer instead of an Int since Int is an AnyVal, which is not nullable by default. Furthermore, I only consider the age field to be nullable, so you might need to adopt as necessary. Hope it helps.
case class Foo (name:String, age: Integer)
object MyJsonProtocol extends DefaultJsonProtocol {
implicit object FooJsonFormat extends RootJsonFormat[Foo] {
def write(foo: Foo) =
JsObject("name" -> JsString(foo.name),
"age" -> Option(foo.age).map(JsNumber(_)).getOrElse(JsNull))
def read(value: JsValue) = value match {
case JsObject(fields) =>
val ageOpt: Option[Integer] = fields.get("age").map(_.toString().toInt) // implicit conversion from Int to Integer
val age: Integer = ageOpt.orNull[Integer]
Foo(fields.get("name").get.toString(), age)
case _ => deserializationError("Foo expected")
}
}
}
import MyJsonProtocol._
import spray.json._
val json = """{ "name": "Meh" }""".parseJson
println(json.convertTo[Foo]) // prints Foo("Meh",null)
It seems you're out of luck
From the doc you linked:
spray-json will always read missing optional members as well as null optional members as None
You can customize the json writing, but not the reading.
I am using Spray-json 1.3.1. I have the following JSON message:
{
"results": [{
... NOT IMPORTANT PART HERE ...
}],
"status": "OK"
}
Trivially, this can be deserialized to status String field via
case class Message[T](results: List[T], status: String)
with custom Protocol
object MessageProtocol extends DefaultJsonProtocol {
implicit def messageFormat[T: JsonFormat] = jsonFormat2(Message.apply[T])
}
Since status field can be one of OK, ZERO_RESULTS, OVER_QUERY_LIMIT having this field as a String makes no sense. As I am coming from
Java background I tried enums in Scala implemented as follows:
case class Message[T](results: List[T], status: Status)
object Status extends Enumeration{
type Status = Value
val OK,ZERO_RESULTS,OVER_QUERY_LIMIT, REQUEST_DENIED, INVALID_REQUEST,UNKNOWN_ERROR = Value
}
object MessageProtocol extends DefaultJsonProtocol {
implicit val statusFormat = jsonFormat(Status)
implicit def messageFormat[T: JsonFormat] = jsonFormat2(Message.apply[T])
}
What is best practice/approach to solve this?
You can simply implement your own RootJsonFormat (as an implicit in Message companion object) and override read and write functions. There you will have JsObject and you can convert it to your own case class as you want like converting the string to desired enumeration etc. You can see a sample here
I want to convert a case class with an Option[DateTime] parameter to a spray-json object which can be served by an API. Using spray-json I have a custom JsonFormat as such
object JsonImplicits extends DefaultJsonProtocol {
implicit object PostJsonFormat extends RootJsonFormat[Post] {
def write(p: Post) = JsObject(
"title" -> JsString(p.title),
"content" -> JsString(p.content),
"author" -> JsString(p.author),
"creationDate" -> JsString(p.creationDate.getOrElse(DateTime.now))
)
}
}
But I get:
overloaded method value apply with alternatives:
(value: String)spray.json.JsString <and>
(value: Symbol)spray.json.JsString
cannot be applied to (com.github.nscala_time.time.Imports.DateTime)
"creationDate" -> JsString(p.creationDate.getOrElse(DateTime.now))
when I try to compile it and no matter what I try I can't seem to convert the DateTime object to a string. For instance, when I try calling toString I get
ambiguous reference to overloaded definition,
both method toString in class AbstractDateTime of type (x$1: String, x$2: java.util.Locale)String
and method toString in class AbstractDateTime of type (x$1: String)String
match expected type ?
"creationDate" -> JsString(p.creationDate.getOrElse(DateTime.now.toString)))
You have several problems here.
First, the toString() method in AbstractDateTime requires one or several arguments see here.
But I would advise you against this path and recommend using properly Spray-Json.
Spray-json does not know how to serialize Option[DateTime], therefore you have to provide a RootJsonFormat for it.
This is what I am doing.
implicit object DateJsonFormat extends RootJsonFormat[DateTime] {
private val parserISO : DateTimeFormatter = ISODateTimeFormat.dateTimeNoMillis();
override def write(obj: DateTime) = JsString(parserISO.print(obj))
override def read(json: JsValue) : DateTime = json match {
case JsString(s) => parserISO.parseDateTime(s)
case _ => throw new DeserializationException("Error info you want here ...")
}
}
Adapt it as you want if you do not want to use ISO formatting.