Our finatra application uses json4s to serialize objects to jsons in our controller responses. However, I noticed that when trying to serialize enums, it creates an empty object.
I saw this response that would resolve my issue but would have to be replicated for each enum: https://stackoverflow.com/a/35850126/2668545
class EnumSerializer[E <: Enum[E]](implicit ct: Manifest[E]) extends CustomSerializer[E](format ⇒ ({
case JString(name) ⇒ Enum.valueOf(ct.runtimeClass.asInstanceOf[Class[E]], name)
}, {
case dt: E ⇒ JString(dt.name())
}))
// first enum I could find
case class X(a: String, enum: java.time.format.FormatStyle)
implicit val formats = DefaultFormats + new EnumSerializer[java.time.format.FormatStyle]()
// {"a":"test","enum":"FULL"}
val jsonString = Serialization.write(X("test", FormatStyle.FULL))
Serialization.read[X](jsonString)
Is there a way to make a generic custom serializer that would handle all java enum instances by grabbing their .name() value when serializing to json?
I don't think there is a clean solution because of the type-safety constraints. Still if you are OK with a hacky solution that relies on the fact that Java uses type erasure, here is one that seems to work:
class EnumSerializer() extends Serializer[Enum[_]] {
override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), Enum[_]] = {
// using Json4sFakeEnum is a huge HACK here but it seems to work
case (TypeInfo(clazz, _), JString(name)) if classOf[Enum[_]].isAssignableFrom(clazz) => Enum.valueOf[Json4sFakeEnum](clazz.asInstanceOf[Class[Json4sFakeEnum]], name)
}
override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
case v: Enum[_] => JString(v.name())
}
}
where Json4sFakeEnum
is really a fake enum
defined in Java (actually any enum
should work but I prefer to make it explicitly fake)
enum Json4sFakeEnum {
}
With such definition an example similar to yours
// first enum I could find
case class X(a: String, enum: java.time.format.FormatStyle)
def js(): Unit = {
implicit val formats = DefaultFormats + new EnumSerializer()
val jsonString = Serialization.write(X("test", FormatStyle.FULL))
println(s"jsonString '$jsonString'")
val r = Serialization.read[X](jsonString)
println(s"res ${r.getClass} '$r'")
}
Produces following output:
jsonString '{"a":"test","enum":"FULL"}'
res class so.Main$X 'X(test,FULL)'
Update or How does it work and why you need Json4sFakeEnum
?
There are 2 important things:
Extending Serializer
instead of CustomSerializer
. This is important because it allows creating a single non-generic instance that can handle all Enum
types. This works because the function created by Serializer.deserialize
receives TypeInfo
as an argument so it can analyze runtime class.
Json4sFakeEnum
hack. From the high-level point of view it is enough to have just a Class
of the given enum to get all names because they are stored in the Class
object. However on the implementation details level the simplest way to access that is to use Enum.valueOf
method that has following signature:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)
The unlucky part here is that it has a generic signature and there is a restriction T extends Enum<T>
. It means that even though we have proper Class
object the best type we know is still Enum[_]
and that doesn't fit the self-referencing restriction of extends Enum<T>
. On the other hand Java uses type erasure so valueOf
is actually compiled to something like
public static Enum<?> valueOf(Class<Enum<?>> enumType, String name)
It means that if we just trick the compiler into allowing us to call valueOf
, at the runtime everything will be alright. And this is where Json4sFakeEnum
comes on the scene: we just need some known at the compile time specific subclass of Enum
to make the valueOf
call.