Search code examples
scalaserializationenumsjson4s

Generically Serialize Java Enums to json using json4s


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?


Solution

  • 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:

    1. 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.

    2. 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.