I'm using json4s 3.2.11 and Scala 2.11.
I have an enumeration defined using sealed trait
, and a custom serializer for it:
import org.json4s.CustomSerializer
import org.json4s.JsonAST.JString
import org.json4s.DefaultFormats
import org.json4s.jackson.Serialization
sealed trait Foo
case object X extends Foo
case object Y extends Foo
object FooSerializer
extends CustomSerializer[Foo](
_ =>
({
case JString("x") => X
case JString("y") => Y
}, {
case X => JString("x")
case Y => JString("y")
})
)
This is great, and works well when added to the formats:
{
implicit val formats = DefaultFormats + FooSerializer
Serialization.write(X) // "x"
}
This is great!
If the serializer is not added to the formats, json4s will use reflection to create a default representation of the fields, which is extremely unhelpful for these object
s that don't have fields. It does this silently, seemingly without a way to control it.
{
implicit val formats = DefaultFormats
Serialization.write(X) // {}
}
This is a problematic, as there's no indication of what's gone wrong until much later. This invalid/useless data might be sent around the network or written to databases, if tests don't happen to catch it. And, this may be exposed publicly from a library, meaning downstream users have to remember it as well.
NB. this is different to read
, which throws an exception on failure, since the Foo
trait doesn't have any useful constructors:
{
implicit val formats = DefaultFormats
Serialization.read[Foo]("\"x\"")
}
org.json4s.package$MappingException: No constructor for type Foo, JString(x)
at org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$constructor(Extraction.scala:417)
at org.json4s.Extraction$ClassInstanceBuilder.org$json4s$Extraction$ClassInstanceBuilder$$instantiate(Extraction.scala:468)
at org.json4s.Extraction$ClassInstanceBuilder$$anonfun$result$6.apply(Extraction.scala:515)
...
Is there a way to either disable the default {}
formatting for these objects, or to "bake" in the formatting to the object itself?
For instance, having write
throw an exception like read
would be fine, as it would flag the problem to the caller immediately.
There is an old open issue which seems to ask similar question where one of the contributors suggests to
you need to create a custom deserializer or serializer
which makes it sound there is no out-of-the-box way to alter the default behaviour.
Try disallowing import of org.json4s.DefaultFormats
using Scalastyle IllegalImportsChecker
<check level="error" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="true">
<parameters>
<customMessage>Import from illegal package: Please use example.DefaultFormats instead of org.json4s.DefaultFormats</customMessage>
<parameter name="illegalImports"><![CDATA[org.json4s.DefaultFormats]]></parameter>
</parameters>
</check>
and provide custom DefaultFormats
like so
package object example {
val DefaultFormats = Serialization.formats(NoTypeHints) + FooSerializer
}
which would allow us to serialise ADTs like so
import example.DefaultFormats
implicit val formats = DefaultFormats
case class Bar(foo: Foo)
println(Serialization.write(Bar(X)))
println(Serialization.write(X))
println(Serialization.write(Y))
which should output
{"foo":"x"}
"x"
"y"
If we try to import org.json4s.DefaultFormats
, then Scalastyle should raise the following error:
Import from illegal package: Please use example.DefaultFormats instead of org.json4s.DefaultFormats
Perhaps we could "bake in" the formatting into objects by defining write
method in Foo
which delegates to Serialization.write
like so
sealed trait Foo {
object FooSerializer extends CustomSerializer[Foo](_ =>
({
case JString("x") => X
case JString("y") => Y
}, {
case X => JString("x")
case Y => JString("y")
})
)
def write: String =
Serialization.write(this)(DefaultFormats + FooSerializer)
}
case object X extends Foo
case object Y extends Foo
Note how we hardcoded passing FooSerializer
format to write
. Now we can serialise with
println(X.write)
println(Y.write)
which should output
"x"
"y"
DefaultFormats
alongside org.json4s.DefaultFormats
We could also try defining custom DefaultFormats
in our own package like so
package example
object DefaultFormats extends DefaultFormats {
override val customSerializers: List[Serializer[_]] = List(FooSerializer)
}
which would allow us to serialise ADTs like so
import example.DefaultFormats
implicit val formats = DefaultFormats
case class Bar(foo: Foo)
println(Serialization.write(Bar(X)))
println(Serialization.write(X))
println(Serialization.write(Y))
which should output
{"foo":"x"}
"x"
"y"
Having two default formats, org.json4s.DefaultFormats
and example.DefaultFormats
, would at least make the user have to choose between the two, if say, they use IDE to auto-import them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With