Skip to content
PJ Fanning edited this page Sep 21, 2021 · 13 revisions

How do I support Scala 2 Enumeration

Enumerations

Deserializing Option[Int], Seq[Int] and other primitive challenges

TL;DR: Use @JsonDeserialize(contentAs = classOf[java.lang.Integer]) or @JsonDeserialize(contentAs = classOf[Int]) on the offending member.

What is happening, and why

Scala, unlike Java, supports using primitive types as type arguments to generic classes. However, this is not supported by the JVM; there is no way to represent java.lang.List<int>, as an example. Prior to Scala 2.9, the compiler would represent Option[Int] as Option[java.lang.Integer] to the JVM, which would appear to naturally fall out of the requirement to use reference types as type parameters.

However, this led to significant problems, for example, implementing bridge methods for traits. The solution, for the time being, is that all primitive type parameters are represented as Object to the JVM. The Scala compiler uses the ScalaSignature annotation to determine what the Scala type needs to be in the cases where it matters.

Enter Jackson, which at its core is a Java library. The Scala module has informed Jackson that Option is effectively a container type, but it relies on Java reflection to determined the contained type, and comes up with Object. Jackson has rules for dealing with this situation, which dictate that the dynamic type of Object values should be the closest natural Java type for the value: java.lang.String for strings, java.lang.Integer, java.lang.Long, or java.math.BigInteger for whole numbers, depending on what size the number fits into, and similarly java.lang.Float, java.lang.Double, or java.lang.BigDecimal for floating point values.

The Scala compiler doesn't know what evil has been done in Java-land. It "knows" the value is a primitive int even if the JVM says Object, so it attempts to cast the value to the relevant boxed type and unbox it. If the primitive type matches the type selected by Jackson, it works; otherwise the actual type protests such treatment in the form of a ClassCastException.

Using alternatives to Option[Int], Seq[Int]

These alternatives will work without issue.

  • Option[BigInt]
  • Option[java.lang.Integer]

JsonDeserialize annotation

The current workaround for this use case is to add the @JsonDeserialize annotation to the member being targeted (Java examples). Specifically, this annotation has a set of parameters that can be used for different situations:

  • contentAs for collections or map values
  • keyAs for Map keys

Examples of how to use this annotation can be found in the tests directory.

Registering a custom deserializer for your class.

Baeldung have some good Jackson docs. The docs focus on Java use cases but they should be usable for Scala use cases. They have one about registering a custom deserializer.

Jackson 2.13.0

jackson-module-scala 2.13.0 adds ScalaAnnotationIntrospector.registerReferencedType - that can be used if you can't annotate the class. This approach is experimental.

Possible future solutions

The best solution for this problem would be to use Scala reflection to determine the Scala type, and tell Jackson which boxed type it should use. This solution is not without issues, for Scala reflection does not support anonymous or method-local types, and isn't a option at all for Scala 2.10, whose reflection API is experimental and is not thread-safe (and cannot be made so). Scala Reflection is no longer fully supported in Scala 3 but there may be alternatives like izumi-reflect and scala-reflection.

At one stage, reading the @ScalaSignature annotation to determine the correct Scala type seemed like an option - but this is not supported in Scala 3.

Scala macros might also be an option but Scala 2 and Scala 3 have very different macro solutions.

Clone this wiki locally