diff --git a/akka-docs/src/main/paradox/serialization-jackson.md b/akka-docs/src/main/paradox/serialization-jackson.md index 1870b5bf30..21b3aa1496 100644 --- a/akka-docs/src/main/paradox/serialization-jackson.md +++ b/akka-docs/src/main/paradox/serialization-jackson.md @@ -164,11 +164,20 @@ when using polymorphic types. ### ADT with trait and case object -In Scala it's common to use a sealed trait and case objects to represent enums. If the values are case classes +It's common in Scala to use a sealed trait and case objects to represent enums. If the values are case classes the `@JsonSubTypes` annotation as described above works, but if the values are case objects it will not. The annotation requires a `Class` and there is no way to define that in an annotation for a `case object`. -This can be solved by implementing a custom serialization for the enums. Annotate the `trait` with +The easiest workaround is to define the case objects as case class without any field. + +Alternatively, you can define an intermediate trait for the case object and a custom deserializer for it. The example below builds on the previous `Animal` sample by adding a fictitious, single instance, new animal, an `Unicorn`. + +Scala +: @@snip [SerializationDocSpec.scala](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #polymorphism-case-object } + +The case object `Unicorn` can't be used in a `@JsonSubTypes` annotation, but its trait can. When serializing the case object we need to know which type tag to use, hence the `@JsonTypeName` annotation on the object. When deserializing, Jackson will only know about the trait variant therefore we need a custom deserializer that returns the case object. + +On the other hand, if the ADT only has case objects, you can solve it by implementing a custom serialization for the enums. Annotate the `trait` with `@JsonSerialize` and `@JsonDeserialize` and implement the serialization with `StdSerializer` and `StdDeserializer`. diff --git a/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala b/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala index 29d19f1101..1ce3b2e2e9 100644 --- a/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala +++ b/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala @@ -12,8 +12,11 @@ import akka.serialization.SerializationExtension import akka.serialization.SerializerWithStringManifest import akka.serialization.Serializers import akka.testkit.TestKit -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.{ JsonSubTypes, JsonTypeInfo, JsonTypeName } +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.typesafe.config.ConfigFactory import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers @@ -126,20 +129,51 @@ object SerializationDocSpec { #//#manifestless """ - //#polymorphism - final case class Zoo(primaryAttraction: Animal) extends MySerializable + object Polymorphism { - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes( - Array( - new JsonSubTypes.Type(value = classOf[Lion], name = "lion"), - new JsonSubTypes.Type(value = classOf[Elephant], name = "elephant"))) - sealed trait Animal + //#polymorphism + final case class Zoo(primaryAttraction: Animal) extends MySerializable - final case class Lion(name: String) extends Animal + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes( + Array( + new JsonSubTypes.Type(value = classOf[Lion], name = "lion"), + new JsonSubTypes.Type(value = classOf[Elephant], name = "elephant"))) + sealed trait Animal - final case class Elephant(name: String, age: Int) extends Animal - //#polymorphism + final case class Lion(name: String) extends Animal + + final case class Elephant(name: String, age: Int) extends Animal + //#polymorphism + } + + object PolymorphismMixedClassObject { + + //#polymorphism-case-object + final case class Zoo(primaryAttraction: Animal) extends MySerializable + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes( + Array( + new JsonSubTypes.Type(value = classOf[Lion], name = "lion"), + new JsonSubTypes.Type(value = classOf[Elephant], name = "elephant"), + new JsonSubTypes.Type(value = classOf[Unicorn], name = "unicorn"))) + sealed trait Animal + + final case class Lion(name: String) extends Animal + final case class Elephant(name: String, age: Int) extends Animal + + @JsonDeserialize(using = classOf[UnicornDeserializer]) + sealed trait Unicorn extends Animal + @JsonTypeName("unicorn") + case object Unicorn extends Unicorn + + class UnicornDeserializer extends StdDeserializer[Unicorn](Unicorn.getClass) { + // whenever we need to deserialize an instance of Unicorn trait, we return the object Unicorn + override def deserialize(p: JsonParser, ctxt: DeserializationContext): Unicorn = Unicorn + } + //#polymorphism-case-object + } val configDateTime = """ #//#date-time @@ -207,6 +241,19 @@ class SerializationDocSpec private def serializerFor(obj: Any): SerializerWithStringManifest = serialization.serializerFor(obj.getClass).asInstanceOf[SerializerWithStringManifest] + "serialize trait + case classes" in { + import doc.akka.serialization.jackson.SerializationDocSpec.Polymorphism._ + verifySerialization(Zoo(Lion("Simba"))) should ===(Zoo(Lion("Simba"))) + verifySerialization(Zoo(Elephant("Dumbo", 1))) should ===(Zoo(Elephant("Dumbo", 1))) + } + + "serialize trait + case classes + case object" in { + import doc.akka.serialization.jackson.SerializationDocSpec.PolymorphismMixedClassObject._ + verifySerialization(Zoo(Lion("Simba"))) should ===(Zoo(Lion("Simba"))) + verifySerialization(Zoo(Elephant("Dumbo", 1))) should ===(Zoo(Elephant("Dumbo", 1))) + verifySerialization(Zoo(Unicorn)) should ===(Zoo(Unicorn)) + } + "serialize trait + object ADT" in { import CustomAdtSerializer.Compass import CustomAdtSerializer.Direction._