It’s been a while since the last major release of Enumeratum, and in 1.4.0, minor changes include Play 2.5 support, integration library version bumps, and small internal refactorings. More excitingly though, the latest version adds support for a new kind of enumeration, ValueEnum, as well as an integration with the Circe JSON library.

Points of interest:

  • Unlike other value enum implementations, Enumeration’s value enums perform uniqueness checks at compile time to make sure you have unique values across your enum members.
  • Circe integration allows you to send and receive JSON data between your front end and your server using the same code

The 1.4.0 release page on Github has a more detailed list of changes, but we’ll specifically go through:

  1. ValueEnums
  2. Circe Integration

ValueEnums

What is a ValueEnum? It’s an enum that represents a primitive value (e.g. Int, Long, Short) instead of a String. I may have just made up the term, but it doesn’t matter as long as you know what I mean.

1
2
3
4
5
6
7
8
// Have something like
object ContentType {
  case object Text(1)
  case object Image(3)
}

// Want to do
assert(ContentType.withValue(3) == ContentType.Image)

This may sound mundane, since you can already build something like this yourself with the standard library’s Enumeration (or previous versions of Enumeratum ), but sometimes the most straightforward solutions are suboptimal.

The trouble with Enumeration

The standard lib’s Enumeration comes with the notion of a customisable id: Int on each member, which is a great starting point for implementing numbers-based enumerations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
  * This is an anti-example to show what can go wrong.
  *
  * Do not use this
  */
object Things extends Enumeration {
  val First = Value(1)
  val Second = Value(2)
  val Third = Value(3)
  val Fourth = Value(3) // not enough coffeeeeee

  def withValueOpt(i: Int): Option[Things.Value] = values.find(_.id == i)
}

/*
 *  The above Enumeration will compile. Fine, but imagine now your app is deployed
 *  and somewhere else in your code you have to actually use your enum.
 */

Things.First // => java.lang.AssertionError: assertion failed: Duplicate id: 3

// If at first you don't succeed???
Things.First // => java.lang.NoClassDefFoundError: Could not initialize class Things$
// newp

This funny behaviour is caused by the fact that Enumeration#Values (First, Second, Third, Fourth) are not checked for unique ids at compile time, and are instantiated when their outer Enumeration object is lazily instantiated. When a Value is instantiated, its id is stuffed into a HashMap[Int, Value] after an assertion check that the id does not already exist in the map.

What has happend in the above example is the enumeration code compiles, but when we call Things.First, object Things gets instantiated, and throws an assertion error when val Fourth is being instantiated with an id of 3, which has already been assigned to Third and thus is already in the aforementioned HashMap. This prevents the singleton Things from getting instantiated, and the next time you try to use it, Scala will throw a NoClassDefFoundError.

One way to work around this is to write tests for every such Enumeration to make sure that no one working in the code base has fat-fingered any ids. I’m a big proponent of writing tests, but tests are also code and come with a maintenance and cognitive cost, so I would prefer not having to write tests to make sure my simple value enums can be safely initialised.

This kind of problem is not limited to Enumeration: careless implementation of something similar may result in arguably freakier outcomes such as silent failures (2 members with the same value but only one of the members can be retrieved by value).

ValueEnum

In version 1.4.0 of Enumeratum, we’ve introduced 3 pairs of traits: IntEnum and IntEnumEntry, LongEnum and LongEnumEntry, and ShortEnum and ShortEnumEntry. As their names suggest, these are value enum traits that allow you to create enums that are backed by Int, Long and Short respectively. Each pair extends ValueEnum and ValueEnumEntry. Note that this class hierarchy is a bit extensive for now, and it may be more streamlined in the future.

This is an example of how you would create an Long based value enum with Play integration (JSON readers and writers, Query string binders, Path binders, Form formatters, etc):

ContentType value enum with full Play integration (ContentType.scala) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import enumeratum.values._

sealed abstract class ContentType(val value: Long, name: String) extends LongEnumEntry

case object ContentType extends LongPlayEnum[ContentType] {

  val values = findValues

  case object Text extends ContentType(value = 1L, name = "text")
  case object Image extends ContentType(value = 2L, name = "image")
  case object Video extends ContentType(value = 3L, name = "video")
  case object Audio extends ContentType(value = 4L, name = "audio")
  /* case object Sticker extends ContentType(value = 4L, name = "audio")
  *   => Fails at compile time because 4L is already used with the following error
  *   It does not look like you have unique values. Found the following values correspond to more than one members: Map(4 -> List(object Audio, object Sticker))
  */

}

assert(ContentType.withValue(1) == ContentType.Text)

ContentType.withValue(10) // => java.util.NoSuchElementException:

// Use with Play-JSON
import play.api.libs.json.{ JsNumber, JsString, Json => PlayJson }
ContentType.values.foreach { item =>
    assert(PlayJson.toJson(item) == JsNumber(item.value))
}

The findValues method of ValueEnums works similarly to the findValues method of Enumeratum’s older Enum, except the macro will ensure that there is a literal value member or constructor for each enum entry and fails the compilation if more than one member shares the same value.

As the above example demonstrates, there are Play (and standalone Play-JSON) integrations available for this new kind of enum, as well as for UPickle, and Circe.

~~Note that this new feature is not yet available in Scala 2.10 and in the REPL due to Macro expansion differences~~ (update: now works in the REPL and is available for 2.10.x!).

Circe integration

Enumeratum 1.4.0 also adds support for serialising/deserialising to JSON using Circe, an up-and-coming performant and feature-filled JSON library published for both JVM and ScalaJS.

This is how you would use Circe with Enumeratum’s Enum (integrations for ValueEnum also exist)

ShirtSize Enum with Circe integration (ShirtSize.scala) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import enumeratum._

sealed trait ShirtSize extends EnumEntry

case object ShirtSize extends CirceEnum[ShirtSize] with Enum[ShirtSize] {

  case object Small extends ShirtSize
  case object Medium extends ShirtSize
  case object Large extends ShirtSize

  val values = findValues

}

import io.circe.Json
import io.circe.syntax._

ShirtSize.values.foreach { size =>
    assert(size.asJson == Json.fromString(size.entryName))
}

Conclusion

Hopefully, Enumeratum’s new ValueEnum implementations will make development easier and safer for engineers out there who need to use value enumerations. Since uniqueness is checked at compile-time, you can save yourself the trouble of writing a bunch of pedantic tests. Circe is a promising JSON library that was really easy to integrate with and I look forward to taking advantage of the fact that it works on both server side and on the front end.

As always, if you have any problems, questions, suggestions, or better yet, PRs, please do not hesitate to get in touch on Github.

Comments