I Like Enums, but I've Stopped Calling Them Closed

ludovicianul1 pts0 comments

I Like Enums, but I’ve Stopped Calling Them Closed | Dochia CLI Blog

Home About<br>RSS Feed

Table of Contents

I really do like enums. They replace strings and integers that could mean anything with a small set of named values that<br>the compiler understands. They make an API inside a codebase easier to discover, keep typos out of branches, and let an<br>IDE show me the entire domain without sending me to documentation. When the set is genuinely finite, an enum feels like<br>the language and the problem agreeing with each other.

I used to ask a Java interview question about the best way to write a singleton. Most answers started with a private<br>constructor and a static field, then accumulated synchronization, double-checked locking, or a holder class. The answer<br>I was looking for was a single-element enum. It is concise, serialization cannot accidentally create a second instance,<br>and reflection cannot invoke an enum constructor to manufacture another one. The JVM can provide those guarantees<br>because an enum makes a strong promise: its instances were decided when the type was compiled.

I also like that Java enums are real classes, not dressed-up integers. They can carry fields and methods, implement<br>interfaces, and even give individual constants their own behavior.[16] EnumSet and EnumMap take advantage of the<br>same finite universe: one can be backed by a bit vector and the other by an array.[17] There is something satisfying<br>about the language using the closed set rather than merely asking me to remember it.

Modern Java switch expressions give me another reason to like them. Switch over an enum without a broad default, and<br>the compiler can tell you when you have forgotten a case. Add a new constant in the same build and every exhaustive<br>switch becomes a small to-do list. That is exactly the kind of work I want a type system to do for me.

All of these benefits come from the same assumption: the compiler knows the whole set.

It took me a while to appreciate how local that assumption is.

Inside one program, an enum is closed because one compiler owns both the definition and its uses. Put the same value on<br>a network and the situation changes. The producer and consumer may have been generated from different schema versions,<br>released by different teams and deployed months apart. I still find it useful to think of the enum as closed in my code,<br>but once it crosses that boundary, it is really just the set of values known so far .

This matters most for response fields: statuses, categories, plan tiers, review outcomes, fulfillment states. A server<br>accepting a new request value is usually additive for old clients; they simply will not send it. A server returning a<br>new response value is different. Every existing consumer has to do something with a value that did not exist when it was<br>built.

Consider a payment status:

pending<br>succeeded<br>failed<br>Sooner or later somebody needs disputed. The producer sees one new item in a schema. An old consumer may see a<br>deserialization error, an unknown placeholder, a missing UI element, or a branch of business logic that never runs.

That is what makes this easy to underestimate: one harmless-looking schema change can fail in several completely<br>different ways.

Adding a value can be a breaking change

OpenAPI makes enums attractive. They improve documentation and autocomplete, and code generators can turn them into Java<br>or C# enums, TypeScript unions, or equivalent native types.

The generated behavior is not consistent across languages or generators. A TypeScript union may disappear entirely at<br>runtime unless a validator enforces it. A generated Java client may reject an unknown value while parsing JSON. Other<br>clients preserve the string or map it to a fallback. OpenAPI Generator and NSwag users have both reported clients<br>failing on values absent from the schema they were generated from.[13][14]

OpenAPI Generator makes the compatibility problem unusually explicit. Its Java generator has an enumUnknownDefaultCase<br>option. The documentation says that, without it, a client can fail to parse a response containing a value added by a<br>newer server. The option is off by default and, when enabled, generates an unknown_default_open_api case.[1]

That option exists because synchronized upgrades are not a realistic assumption for a public API. You can ship<br>disputed today while an integration partner is still using an SDK generated six months ago. If its decoder is strict,<br>every webhook containing the new status may fail before the partner’s application code even sees it.

This argument has been going on for years, and I understand why. A GraphQL issue asking whether adding an enum member<br>should count as a breaking change ended up circling the familiar disagreement: the schema change looks additive, but<br>clients often write exhaustive handling based on the old schema.[2]

Stripe takes the operational view. One of its versioned changelog entries puts newly added enum values under “Breaking<br>changes” and...

enum like enums java schema closed

Related Articles