Read more at, or return to the homepage

Enum Apex Class Gotchas

The abstract enum class in Apex can be very helpful as a class-like object, but there are a few things you should keep in mind to avoid getting bitten by the use of enums. I was bitten hard by the use of enums recently, so I’d like to go a bit into depth on these shady, ill-documented characters; their usages, where they shine, and where you too might get into gray areas using them.

What are enums? That’s a tricky question to answer. Here's the official documentation description:

An enum is an abstract data type with values that each take on exactly one of a finite set of identifiers that you specify. Enums are typically used to define a set of possible values that don’t otherwise have a numerical order, such as the suit of a card, or a particular season of the year.

The easy way to think of them are compile-time "constants" that can be statically typed. They’re sort of like a list of static strings, in that sense, and they obey some interesting rules in addition to the basics:

Enums can be defined within a class or in a separate .cls file with the following notation:

1link//or global

2linkpublic enum ExampleEnum { FirstValue, SecondValue }

All enums have access to two methods: name() and ordinal() - name simply returns the string version of your enum instance, ordinal returns its index in the list.

Enums are sealed; you can’t add further methods or attributes to an enum. In other words ... they’re sort of a poor man’s class. The latest version of Java actually offers up a different take on this specifically for POJOs (plain old Java objects, sometimes called "beans" as well ... ): records. I mention this specifically because Enums, like these new Record types, are a derivative of the plain Object class; they just can't be tested for using something like instanceof (try it yourself in Apex: System.debug(LoggingLevel.ERROR instanceof Enum); will sadly fail to compile), yet they do have hidden implementations of the basic Object methods, hashCode() and equals().

The rather excellent documentation on using custom types as Map keys or within Sets is for sure worth a read if you're looking to understand why these methods matter; it's also particularly important if you're looking to implement custom sorting using the Comparable interface in Apex

So why do people like to use enums? They offer better type-safety than the use of static strings. For another, they self-encapsulate their data; while the use of a static string potentially says something about what you’re interacting with, in reality the VALUE behind that string can change - either with reassignment, if the variable isn’t listed as final, or simply by changing the code.

Plus, in an if/else or switch statement, testing against an enum’s value gives the developer the pleasure of understanding immediately the statement being evaluated at hand. You don’t have to go look up the value of that string - it’s right there in front of you! It’s a great mechanism for capturing certain known conditions; while the docs use the example of Seasons, and cardinal directions, perhaps a better example might be types of errors returned from an API (if they come in string form ...).

So ... that all seems pretty good, actually. What are the downsides to enums, then?

linkUsing Enums In Data Classes

For one — you might be tempted (as I was) to use enums in other languages when communicating to Salesforce via API. Spoiler alert: that’s not gonna work. You CAN include an enum (or even a list of enum values!) as a property on an object being used in an Apex endpoint:

1linkpublic enum Season { WINTER, SPRING, SUMMER, FALL }


3linkpublic class DataObject {

4link public Integer MyProperty { get; set }

5link public Season MySeason { get; set; }



8link//... in your web service


10linkglobal static String post(DataObject data) {

11link //.. do something now that your object's been correctly deserialized



What you can't, under any circumstances do, is try to send an enum to Salesforce to represent that the corresponding MySeason on the data object. You have to send a string instead. Otherwise your service is going to throw an error trying to deserialize your object — ouch. The reason for that will become abundantly clear as I walk you through this next part - the danger of using enums as key values in Map instances with Salesforce.

linkEnums in Batchable & Queueable Apex

When can we reference prior values in a new execution context? Within Batchable and Queueable Apex! Let’s dive in to a simple example (one of the reasons I loathe Batchable Apex — this "simple" example takes up quite a bit of real estate):

I probably account for 300+ of the (at this moment) 541 views of this question on the Salesforce Stack Exchange, which first alerted me to this issue several years ago. I hadn't personally been bitten by this particular issue; at the time I was writing a custom equality library for Apex and I was trying to figure out if there was a graceful way to handle Enums.

1linkpublic class EnumBatchableExample implements Database.Batchable<SObject> {


3link public enum Direction { NORTH, SOUTH, EAST, WEST }


5link public class TestIterable implements Iterable<SObject> {

6link public Iterator<SObject> Iterator() {

7link return new List<SObject>().iterator();

8link }

9link }


11link @testVisible private static Integer firstHashCode;

12link @testVisible private static Integer secondHashCode;

13link @testVisible private static Map<Direction, Integer> directionToNumber = new Map<Direction, Integer>();


15link public Iterable<SObject> start(Database.BatchableContext bc) {

16link System.debug('Starting EnumBatchableExample');


18link Direction north = Direction.NORTH;

19link Direction south = Direction.SOUTH;

20link firstHashCode = north.hashCode();

21link secondHashCode = south.hashCode();

22link directionToNumber.put(north, firstHashCode);

23link directionToNumber.put(south, secondHashCode);


25link System.debug('String version of north: ' + String.valueOf(Direction.NORTH));

26link System.debug('String verison of south: ' + String.valueOf(Direction.SOUTH));

27link System.debug('String verison of this: ' + String.valueOf(this));


29link return new TestIterable();

30link }


32link public void execute(Database.BatchableContext bc, List<SObject> records) {}


34link public void finish(Database.BatchableContext bc) {

35link System.debug('Current map values: ' + directionToNumber);

36link System.debug('EnumBatchableExample finished');

37link }


And then the tests:


2linkprivate class EnumBatchableExampleTests {

3link static Integer northHashcode = EnumBatchableExample.Direction.NORTH.hashCode();

4link static Integer southHashcode = EnumBatchableExample.Direction.SOUTH.hashCode();


6link static EnumBatchableExample.Direction north = EnumBatchableExample.Direction.NORTH;

7link static EnumBatchableExample.Direction south = EnumBatchableExample.Direction.SOUTH;


9link @TestSetup

10link static void setup() {

11link //Ids consistently work as map keys ... is that because

12link //the hashCode is stable between execution contexts?

13link insert new Account(Name = 'EnumBatchableTest');

14link }


16link @isTest

17link static void it_should_retain_hashcode_references() {

18link Account acc = [SELECT Id FROM Account];

19link System.debug('Account Id\'s hashCode for this run: ' + ((Object)acc.Id).hashCode());


21link Database.Batchable<SObject> batchable = new EnumBatchableExample();

22link System.debug('Batchable\'s hashCode for this run: ' + ((Object)batchable).hashCode());

23link System.debug('Custom\'s hashCode for this run: ' + ((Object)new CustomHashCode()).hashCode());


25link Test.startTest();

26link Database.executeBatch(batchable);

27link Test.stopTest();


29link System.assertEquals(northHashcode, EnumBatchableExample.firstHashCode);

30link System.assertEquals(southHashcode, EnumBatchableExample.Direction.SOUTH.hashCode());

31link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(north));

32link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(south));

33link }


35link @isTest

36link static void it_should_retain_hashcode_references_again() {

37link Account acc = [SELECT Id FROM Account];

38link System.debug('Account Id\'s hashCode for this run: ' + ((Object)acc.Id).hashCode());


40link Database.Batchable<SObject> batchable = new EnumBatchableExample();

41link System.debug('Batchable\'s hashCode for this run: ' + ((Object)batchable).hashCode());

42link System.debug('Custom\'s hashCode for this run: ' + ((Object)new CustomHashCode()).hashCode());


44link Test.startTest();

45link Database.executeBatch(batchable);

46link Test.stopTest();


48link System.assertEquals(northHashcode, EnumBatchableExample.firstHashCode);

49link System.assertEquals(southHashcode, EnumBatchableExample.Direction.SOUTH.hashCode());

50link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(north));

51link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(south));

52link }


54link private class CustomHashCode {

55link public Integer hashCode() {

56link return 1;

57link }

58link }


Both of these tests pass, no problem, and there's no way to "force" the issue because with both Batchable & Queueable Apex, you can't actually test the recursion of these jobs within tests. That said, I can show you the results:

Apex enum debug log

There's a lot to go through:

Intuitively, we have to expect that ".equals(Object o)" is overridden on the SObject class. You can find more tantalizing info about how exactly SObject equality works in one very specific place in the Apex Developer Guide: Understanding Expression Operators. Look for the keywords "Equality Operator", which in tantalizing brevity details what happens when you call "==" on something within Apex. There's also a great ribbing on the SOQL team, for good measure.

linkFeeling A Little Enum

TL;DR, what have we learned?

1linkpublic class MyClass {

2link public MyClass(Enum someEnum) {

3link //some detecting of which enum it is here, I guess

4link }


Lastly, much later on I found another highly unusual property of enums while writing the article Replacing DLRS With Custom Rollup: while you can deserialize String-based representations of enums via HTTP endpoints (as we discussed above), you can't do the same thing from within Apex:

1linkpublic enum Season { WINTER }


3linkSystem.debug(JSON.serialize(Season.WINTER)); // outputs "WINTER"

4linkString someString = '{"WINTER" : "hi"}';


6linkSeason myWinterSeason = (Season)JSON.deserialize(someString, Season.class);

7link// throws: System.JSONException: Duplicate field: Season.WINTER. This is a classic "wat" if I ever saw one.



10linksomeString = '{"Season": "WINTER"}';

11link// outputs null

12linkSeason myWinterSeason = (Season)JSON.deserialize(someString, Season.class)

13link// or even crazier! I don't know; I figured maybe some quotes would help

14linksomeString = '{"Season": "\'WINTER\'"}';

15link// outputs null

16linkSeason myWinterSeason = (Season)JSON.deserialize(someString, Season.class)

What you can do is assign that String variable to a property in a wrapper class -- now you can deserialize like a sane person:

1linkpublic class SeasonWrapper {

2link public Season Season { get; set; }




6linkString someString = '{"Season": "WINTER"}';


8linkSeasonWrapper myWinterSeason = (SeasonWrapper)JSON.deserialize(someString, SeasonWrapper.class);


10link// ouputs: SeasonWrapper:[Season=WINTER]

In the end, for the replacing DLRS article and source code, I found that building a Map<String, Op> (don't miss the article for more info on what that Op enum is!) felt less bad than creating a wrapper class to deserialize to.

The original version of Apex Enum Class Gotchas can be read on my blog.

Using Enums In Data ClassesEnums in Batchable & Queueable ApexFeeling A Little Enum

Home Advanced Logging Using Nebula Logger Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Formula Date Issues Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Replacing DLRS With Custom Rollup Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions The Tao Of Apex Writing Performant Apex Tests

Read more tech articles