Skip to content

Commit f51da43

Browse files
authored
Scala3 enum support (#660)
* add scala3 enum support * imports * some tests * adt tests
1 parent 61219dd commit f51da43

File tree

14 files changed

+395
-0
lines changed

14 files changed

+395
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.fasterxml.jackson.module.scala
2+
3+
import com.fasterxml.jackson.module.scala.deser.{ScalaNumberDeserializersModule, ScalaObjectDeserializerModule, UntypedObjectDeserializerModule}
4+
import com.fasterxml.jackson.module.scala.introspect.ScalaAnnotationIntrospectorModule
5+
6+
/**
7+
* Complete module with support for all features, with the exception of [[BitSetDeserializerModule]].
8+
*
9+
* This class aggregates all of the feature modules into a single concrete class.
10+
* Its use is recommended for new users and users who want things to "just work".
11+
* If more customized support is desired, consult each of the constituent traits.
12+
*
13+
* @see [[com.fasterxml.jackson.module.scala.JacksonModule]]
14+
*
15+
* @since 1.9.0
16+
*/
17+
class DefaultScalaModule
18+
extends JacksonModule
19+
with IteratorModule
20+
with EnumerationModule
21+
with OptionModule
22+
with SeqModule
23+
with IterableModule
24+
with TupleModule
25+
with MapModule
26+
with SetModule
27+
with ScalaNumberDeserializersModule
28+
with ScalaObjectDeserializerModule
29+
with ScalaAnnotationIntrospectorModule
30+
with UntypedObjectDeserializerModule
31+
with EitherModule
32+
with SymbolModule
33+
{
34+
override def getModuleName: String = "DefaultScalaModule"
35+
}
36+
37+
object DefaultScalaModule extends DefaultScalaModule

src/main/scala/com/fasterxml/jackson/module/scala/DefaultScalaModule.scala renamed to src/main/scala-3/com/fasterxml/jackson/module/scala/DefaultScalaModule.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class DefaultScalaModule
2222
extends JacksonModule
2323
with IteratorModule
2424
with EnumerationModule
25+
with EnumModule // Scala 3 only
2526
with OptionModule
2627
with SeqModule
2728
with IterableModule
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.fasterxml.jackson.module.scala
2+
3+
import com.fasterxml.jackson.module.scala.deser.EnumDeserializerModule
4+
import com.fasterxml.jackson.module.scala.ser.EnumSerializerModule
5+
6+
trait EnumModule extends EnumSerializerModule with EnumDeserializerModule {
7+
override def getModuleName: String = "EnumModule"
8+
}
9+
10+
object EnumModule extends EnumModule
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package com.fasterxml.jackson.module.scala.deser
2+
3+
import com.fasterxml.jackson.core.JsonParser
4+
import com.fasterxml.jackson.databind.deser.{Deserializers, KeyDeserializers}
5+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
6+
import com.fasterxml.jackson.databind._
7+
import com.fasterxml.jackson.module.scala.JacksonModule
8+
9+
import java.lang.reflect.InvocationTargetException
10+
import scala.languageFeature.postfixOps
11+
import scala.reflect.Enum
12+
import scala.util.Try
13+
14+
private object EnumDeserializerShared {
15+
val IntClass = classOf[Int]
16+
val StringClass = classOf[String]
17+
val EnumClass = classOf[Enum]
18+
19+
def tryValueOf(clz: Class[_], key: String): Option[_] = {
20+
Try(clz.getMethod("valueOf", EnumDeserializerShared.StringClass)).toOption.map { method =>
21+
method.invoke(None.orNull, key)
22+
}
23+
}
24+
25+
def matchBasedOnOrdinal(clz: Class[_], key: String): Option[_] = {
26+
val className = clz.getName
27+
val companionObjectClassOption = if (className.endsWith("$")) {
28+
Some(clz)
29+
} else {
30+
Try(Class.forName(className + "$")).toOption
31+
}
32+
companionObjectClassOption.flatMap { companionObjectClass =>
33+
Try(companionObjectClass.getField("MODULE$")).toOption.flatMap { moduleField =>
34+
val instance = moduleField.get(None.orNull)
35+
Try(clz.getMethod("fromOrdinal", IntClass)).toOption.flatMap { method =>
36+
var i = 0
37+
var matched: Option[_] = None
38+
var complete = false
39+
while (!complete) {
40+
try {
41+
val enumValue = method.invoke(instance, i)
42+
if (enumValue.toString == key) {
43+
matched = Some(enumValue)
44+
complete = true
45+
}
46+
} catch {
47+
case _: NoSuchElementException => {
48+
matched = None
49+
complete = true
50+
}
51+
case itex: InvocationTargetException => {
52+
Option(itex.getCause) match {
53+
case Some(e) if e.isInstanceOf[NoSuchElementException] => {
54+
matched = None
55+
complete = true
56+
}
57+
case Some(e) => throw e
58+
case _ => throw itex
59+
}
60+
}
61+
}
62+
i += 1
63+
}
64+
matched
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
private case class EnumDeserializer[T <: Enum](clazz: Class[T]) extends StdDeserializer[T](clazz) {
72+
private val clazzName = clazz.getName
73+
74+
override def deserialize(p: JsonParser, ctxt: DeserializationContext): T = {
75+
val result = Option(p.getValueAsString).flatMap { text =>
76+
val objectClassOption = if(clazzName.endsWith("$")) {
77+
Try(Class.forName(clazzName.substring(0, clazzName.length - 1))).toOption
78+
} else {
79+
Some(clazz)
80+
}
81+
objectClassOption.flatMap { objectClass =>
82+
Try {
83+
EnumDeserializerShared.tryValueOf(objectClass, text)
84+
.orElse(EnumDeserializerShared.matchBasedOnOrdinal(objectClass, text))
85+
}.toOption.flatten
86+
}.asInstanceOf[Option[T]]
87+
}
88+
result.getOrElse(throw new IllegalArgumentException(s"Failed to create Enum instance for ${p.getValueAsString}"))
89+
}
90+
}
91+
92+
private case class EnumKeyDeserializer[T <: Enum](clazz: Class[T]) extends KeyDeserializer {
93+
private val clazzName = clazz.getName
94+
95+
override def deserializeKey(key: String, ctxt: DeserializationContext): AnyRef = {
96+
val objectClassOption = if(clazzName.endsWith("$")) {
97+
Try(Class.forName(clazzName.substring(0, clazzName.length - 1))).toOption
98+
} else {
99+
Some(clazz)
100+
}
101+
val result = objectClassOption.flatMap { objectClass =>
102+
Try {
103+
EnumDeserializerShared.tryValueOf(objectClass, key)
104+
.orElse(EnumDeserializerShared.matchBasedOnOrdinal(objectClass, key))
105+
}.toOption.flatten
106+
}
107+
val enumResult = result.getOrElse(throw new IllegalArgumentException(s"Failed to create Enum instance for $key"))
108+
enumResult.asInstanceOf[AnyRef]
109+
}
110+
}
111+
112+
private object EnumDeserializerResolver extends Deserializers.Base {
113+
override def findBeanDeserializer(javaType: JavaType, config: DeserializationConfig, beanDesc: BeanDescription): JsonDeserializer[Enum] =
114+
if (EnumDeserializerShared.EnumClass isAssignableFrom javaType.getRawClass)
115+
EnumDeserializer(javaType.getRawClass.asInstanceOf[Class[Enum]])
116+
else None.orNull
117+
}
118+
119+
private object EnumKeyDeserializerResolver extends KeyDeserializers {
120+
override def findKeyDeserializer(javaType: JavaType, config: DeserializationConfig, beanDesc: BeanDescription): KeyDeserializer =
121+
if (EnumDeserializerShared.EnumClass isAssignableFrom javaType.getRawClass)
122+
EnumKeyDeserializer(javaType.getRawClass.asInstanceOf[Class[Enum]])
123+
else None.orNull
124+
}
125+
126+
trait EnumDeserializerModule extends JacksonModule {
127+
override def getModuleName: String = "EnumDeserializerModule"
128+
this += { _ addDeserializers EnumDeserializerResolver }
129+
this += { _ addKeyDeserializers EnumKeyDeserializerResolver }
130+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.fasterxml.jackson.module.scala.ser
2+
3+
import com.fasterxml.jackson.core.JsonGenerator
4+
import com.fasterxml.jackson.databind.ser.Serializers
5+
import com.fasterxml.jackson.databind._
6+
import com.fasterxml.jackson.databind.deser.KeyDeserializers
7+
import com.fasterxml.jackson.module.scala.JacksonModule
8+
9+
import scala.languageFeature.postfixOps
10+
import scala.reflect.Enum
11+
12+
private object EnumSerializerShared {
13+
val EnumClass = classOf[Enum]
14+
}
15+
16+
private object EnumSerializer extends JsonSerializer[Enum] {
17+
def serialize(value: Enum, jgen: JsonGenerator, provider: SerializerProvider): Unit =
18+
provider.defaultSerializeValue(value.toString, jgen)
19+
}
20+
21+
private object EnumKeySerializer extends JsonSerializer[Enum] {
22+
def serialize(value: Enum, jgen: JsonGenerator, provider: SerializerProvider): Unit =
23+
jgen.writeFieldName(value.toString)
24+
}
25+
26+
private object EnumSerializerResolver extends Serializers.Base {
27+
override def findSerializer(config: SerializationConfig, javaType: JavaType, beanDesc: BeanDescription): JsonSerializer[Enum] =
28+
if (EnumSerializerShared.EnumClass.isAssignableFrom(javaType.getRawClass))
29+
EnumSerializer
30+
else None.orNull
31+
}
32+
33+
private object EnumKeySerializerResolver extends Serializers.Base {
34+
override def findSerializer(config: SerializationConfig, javaType: JavaType, beanDesc: BeanDescription): JsonSerializer[Enum] =
35+
if (EnumSerializerShared.EnumClass isAssignableFrom javaType.getRawClass)
36+
EnumKeySerializer
37+
else None.orNull
38+
}
39+
40+
trait EnumSerializerModule extends JacksonModule {
41+
override def getModuleName: String = "EnumSerializerModule"
42+
this += { _ addSerializers EnumSerializerResolver }
43+
this += { _ addKeySerializers EnumKeySerializerResolver }
44+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.fasterxml.jackson.module.scala.`enum`
2+
3+
case class Car(make: String, color: ColorEnum)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.fasterxml.jackson.module.scala.`enum`
2+
3+
enum ColorEnum { case Red, Green, Blue }
4+
5+
case class Colors(set: Set[ColorEnum])
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.fasterxml.jackson.module.scala.`enum`
2+
3+
object Ctx {
4+
enum ColorEnum { case Red, Green, Blue }
5+
}
6+
7+
case class CtxCar(make: String, color: Ctx.ColorEnum)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.fasterxml.jackson.module.scala.`enum`
2+
3+
import com.fasterxml.jackson.core.`type`.TypeReference
4+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
5+
import com.fasterxml.jackson.databind.json.JsonMapper
6+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
7+
import org.scalatest.matchers.should.Matchers
8+
import org.scalatest.wordspec.AnyWordSpec
9+
10+
class EnumDeserializerSpec extends AnyWordSpec with Matchers {
11+
private val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build()
12+
13+
"EnumModule" should {
14+
"deserialize ColorEnum" in {
15+
val red = s""""${ColorEnum.Red}""""
16+
mapper.readValue(red, classOf[ColorEnum]) shouldEqual ColorEnum.Red
17+
}
18+
"fail deserialization of invalid ColorEnum" in {
19+
val json = s""""xyz""""
20+
intercept[IllegalArgumentException] {
21+
mapper.readValue(json, classOf[ColorEnum])
22+
}
23+
}
24+
"deserialize Colors" in {
25+
val colors = Colors(Set(ColorEnum.Red, ColorEnum.Green))
26+
val json = mapper.writeValueAsString(colors)
27+
mapper.readValue(json, classOf[Colors]) shouldEqual colors
28+
}
29+
"deserialize ColorEnum with non-singleton EnumModule" in {
30+
val red = s""""${ColorEnum.Red}""""
31+
mapper.readValue(red, classOf[ColorEnum]) shouldEqual ColorEnum.Red
32+
}
33+
"deserialize JavaCompatibleColorEnum" in {
34+
mapper.writeValueAsString(JavaCompatibleColorEnum.Red) shouldEqual s""""${JavaCompatibleColorEnum.Red}""""
35+
}
36+
"deserialize Car with ColorEnum" in {
37+
val red = s"""{"make":"Perodua","color":"${ColorEnum.Green}"}"""
38+
mapper.readValue(red, classOf[Car]) shouldEqual Car("Perodua", ColorEnum.Green)
39+
}
40+
"deserialize CtxCar with Ctx.ColorEnum" in {
41+
val red = s"""{"make":"Perodua","color":"${Ctx.ColorEnum.Green}"}"""
42+
mapper.readValue(red, classOf[CtxCar]) shouldEqual CtxCar("Perodua", Ctx.ColorEnum.Green)
43+
}
44+
"deserialize Enum as Map Key" in {
45+
val json = s"""{"Green":"green","Red":"red"}"""
46+
val map = mapper.readValue(json, new TypeReference[Map[ColorEnum, String]] {})
47+
map should have size 2
48+
map(ColorEnum.Green) shouldEqual "green"
49+
map(ColorEnum.Red) shouldEqual "red"
50+
}
51+
52+
}
53+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.fasterxml.jackson.module.scala.`enum`
2+
3+
import com.fasterxml.jackson.databind.json.JsonMapper
4+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import org.scalatest.matchers.should.Matchers
6+
import org.scalatest.wordspec.AnyWordSpec
7+
8+
class EnumSerializerSpec extends AnyWordSpec with Matchers {
9+
private val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build()
10+
11+
"EnumModule" should {
12+
"not serialize None" in {
13+
mapper.writeValueAsString(None) should not equal s""""$None""""
14+
}
15+
"serialize ColorEnum" in {
16+
mapper.writeValueAsString(ColorEnum.Red) shouldEqual s""""${ColorEnum.Red}""""
17+
}
18+
"serialize Colors" in {
19+
val json = mapper.writeValueAsString(Colors(Set(ColorEnum.Red, ColorEnum.Green)))
20+
json should startWith("""{"set":[""")
21+
json should include(""""Red"""")
22+
json should include(""""Green"""")
23+
}
24+
"serialize ColorEnum with non-singleton EnumModule" in {
25+
mapper.writeValueAsString(ColorEnum.Red) shouldEqual s""""${ColorEnum.Red}""""
26+
}
27+
"serialize JavaCompatibleColorEnum" in {
28+
mapper.writeValueAsString(ColorEnum.Red) shouldEqual s""""${ColorEnum.Red}""""
29+
}
30+
"serialize Car with ColorEnum" in {
31+
mapper.writeValueAsString(Car("Perodua", ColorEnum.Green)) shouldEqual s"""{"make":"Perodua","color":"${ColorEnum.Green}"}"""
32+
}
33+
"serialize CtxCar with Ctx.ColorEnum" in {
34+
mapper.writeValueAsString(CtxCar("Perodua", Ctx.ColorEnum.Green)) shouldEqual s"""{"make":"Perodua","color":"${Ctx.ColorEnum.Green}"}"""
35+
}
36+
"serialize Enum as Map Key" in {
37+
mapper.writeValueAsString(Map(ColorEnum.Green -> "green")) shouldEqual s"""{"Green":"green"}"""
38+
}
39+
}
40+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.fasterxml.jackson.module.scala.`enum`
2+
3+
enum JavaCompatibleColorEnum extends java.lang.Enum[JavaCompatibleColorEnum] { case Red, Green, Blue }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.fasterxml.jackson.module.scala.`enum`.adt
2+
3+
import com.fasterxml.jackson.databind.json.JsonMapper
4+
import com.fasterxml.jackson.module.scala.DefaultScalaModule
5+
import org.scalatest.matchers.should.Matchers
6+
import org.scalatest.wordspec.AnyWordSpec
7+
8+
class AdtDeserializerSpec extends AnyWordSpec with Matchers {
9+
private val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build()
10+
11+
"EnumModule" should {
12+
"deserialize Color ADT" in {
13+
val red = s""""${Color.Red}""""
14+
mapper.readValue(red, classOf[Color]) shouldEqual Color.Red
15+
}
16+
"fail deserialization of invalid Color ADT" in {
17+
val json = s""""xyz""""
18+
intercept[IllegalArgumentException] {
19+
mapper.readValue(json, classOf[Color])
20+
}
21+
}
22+
"deserialize ColorSet" in {
23+
val colors = ColorSet(Set(Color.Red, Color.Green))
24+
val json = mapper.writeValueAsString(colors)
25+
mapper.readValue(json, classOf[ColorSet]) shouldEqual colors
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)