Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Semyon Kirekov
Semyon Kirekov

Posted on • Edited on

     

Forward Compatible Enum Values in API with Java Jackson

I got an inspiration for this article after watchingthis amazing tech talk by Ilya Sazonov and Fedor Sazonov. If you know Russian, go check it out. It's worth it.

In this article, I'm telling you:

  1. Why do you need to care about forward compatible enum values?
  2. What are the ways to achieve it?
  3. How canJackson library help you out?

Meme cover

Suppose we develop the service that consumes data from one input (e.g.Apache Kafka,RabbitMQ, etc.), deduplicates messages, and produces the result to some output. Look at the diagram below that describes the process.

Deduplication service example

As you can see, the service resolves deduplication rules by theplatform field value.

  1. If the platform isWEB, deduplicate all the messages in 2 hours window.
  2. If the platform isMOBILE, deduplicate all the messages in 3 days window.
  3. Otherwise, proceed with the message flow as-is.

We’re not discussing the technical details behind the deduplication process. It could beApache Flink,Apache Spark, orKafka Streams. Anyway, it’s out of the scope of this article.

Regular enum issue

Seems like the simple Java enum is a perfect candidate to map theplatform field. Look at the code example below.

publicenumPlatform{WEB,MOBILE}
Enter fullscreen modeExit fullscreen mode

On the one hand, we have a strongly typedplatform field which helps to check possible errors during compilation. Also, we can add new enum values easily.

Isn’t that a brilliant approach? Actually, it is not. We’ve forgotten the third rule of deduplication. It says thatunknown platform value shouldnot trigger any deduplications butproceed with the message flow as-is. Then what happens if the service consumes such a message as below:

{"platform":"SMART_TV",...}
Enter fullscreen modeExit fullscreen mode

There is a new platform calledSMART_TV. Nevertheless, no one has warned us that we need to deal with the new value. Because the third rule of deduplication should cover this scenario, right? However, in that case we’d got a deserialization error that would lead to unexpected termination of the message flow.

UKNOWN value antipattern

What can we do about it? Sometimes developers tend to add specialUNKNOWN value to handle such errors. Look at the code example below.

publicenumPlatform{WEB,MOBILE,UNKNOWN}
Enter fullscreen modeExit fullscreen mode

Everything seems great. If theplatform fields has some unexpected string, just map it toPlatform.UNKNOWN value. Anyway, it means that the output message topic received a corruptedplatform field. Look at the diagram below.

Enum UNKNOWN value issue

Though we haven’t applied any deduplication rules, the client received an erased value of theplatform field. Sometimes that’s OK, but not in this case. Thededuplication-service is just a middleware that should not put any unexpected modifications to the submitted message flow. Therefore, theUNKNOWN value is not an option.

Besides, theUNKNOWN presence has some design drawbacks as well. As long as it’s an actual value, one can accidentally use it with inappropriate behavior. For example, you may want to traverse all existing enum values withPlatform.values(). But theUNKNOWN is not the one that you wish to use in your code. As a matter of fact, avoid introducingUNKNOWN enum values at all costs.

String typing

What if we don't use enum at all but just deal with the plainString value? In that case, the clients can assign any string to theplatform field without breaking the pipeline. That's a valid approach, if you don't have to introduce any logic based on the provided value. But we provided some deduplication rules depending on the platform. Meaning that string literals like"WEB" or"MOBILE" ought to repeat through the code.

The problems don't end here. Imagine that the client sent additional requirements to theplatform field determination:

  1. The value should be treated as case insensitive. So,"WEB","web", and"wEB" string are all treated like theWEB platform.
  2. Trailing spaces should be omitted. It means that the" mobile “ value truncates to”mobile" string and converts to theMOBILE platform.

Now the code may look like this:

varresolvedPlatform=message.getPlatform().toUpperCase().trim();if("WEB".equals(resolvedPlatform)){...}elseif("MOBILE".equals(resolvedPlatform)){...}...
Enter fullscreen modeExit fullscreen mode

Firstly, this code snippet is rather smelly. Secondly, the compiler cannot track possible errors due to string typing usage. So, there is a higher chance of making a mistake.

As you can see, string typing solves the issue with the forward compatibility, but still it’s not a perfect approach.

Forward compatible enums

Thankfully, Jackson provides a great mechanism to deal with unknown enum values much cleaner. At first we should create an interfacePlatform. Look at the code snippet below.

publicinterfacePlatform{Stringvalue();}
Enter fullscreen modeExit fullscreen mode

As you can see, the implementations encapsulate the string value that the client passed through the input message queue.

Then we declare a regular enum implementation as an inner static class. Look at the code example below.

publicclassEnumimplementsPlatform{WEB,MOBILE;publicstaticEnumparse(StringrawValue){if(rawValue==null){thrownewIllegalArgumentException("Raw value cannot be null");}vartrimmed=rawValue.toUpperString().trim();for(EnumenumValue:values()){if(enumValue.name().equals(trimmed)){returnenumValue;}}thrownewIllegalArgumentException("Cannot parse enum from raw value: "+rawValue);}@Override@JsonValuepublicStringvalue(){returnname();}}
Enter fullscreen modeExit fullscreen mode

That's a regular Java enum we've seen before. Though there are some details I want to point out:

  1. The Jackson@JsonValue tells the library to serialize the whole object as the result of a single method invocation. Meaning that Jackson always serializesPlatform.Enum as the result of thevalue() method.
  2. We're going to use the staticparse method to obtain enum value from the rawString input.

And now we're creating anotherPlatform implementation to carry unexpected platform values. Look at the code example below.

@ValuepublicclassSimpleimplementsPlatform{Stringvalue;@Override@JsonValuepublicStringvalue(){returnvalue;}}
Enter fullscreen modeExit fullscreen mode

TheValue is the annotation from the Lombok library. It generatesequals,hashCode,toString,getters and marks all the fields asprivate andfinal.

Just a dummy container for the raw string.

After addingEnum andSimple implementations, let's also create a static factory method to create thePlatform from the provided input. Look at the code snippet below.

publicinterfacePlatform{Stringvalue();staticPlatformof(Stringvalue){try{returnPlatform.Enum.parse(value);}catch(IllegalArgumentExceptione){returnnewSimple(value);}}}
Enter fullscreen modeExit fullscreen mode

The idea is trivial. Firstly, we’re trying to create thePlatform as a regular enum value. If parsing fails, then theSimple wrapper returns.

Finally, time to bind all the things together. Look at the Jackson deserializer code below.

classDeserializerextendsStdDeserializer<Platform>{protectedDeserializer(){super(Platform.class);}@OverridepublicPlatformdeserialize(JsonParserp,DeserializationContextctx)throwsIOException{returnPlatform.of(p.getValueAsString());}}
Enter fullscreen modeExit fullscreen mode

Look at the wholePlatform declaration below to summarize the experience.

publicinterfacePlatform{Stringvalue();staticPlatformof(Stringvalue){try{returnPlatform.Enum.parse(value);}catch(IllegalArgumentExceptione){returnnewSimple(value);}}publicclassEnumimplementsPlatform{WEB,MOBILE;publicstaticEnumparse(StringrawValue){if(rawValue==null){thrownewIllegalArgumentException("Raw value cannot be null");}vartrimmed=rawValue.toUpperString().trim();for(EnumenumValue:values()){if(enumValue.name().equals(trimmed)){returnenumValue;}}thrownewIllegalArgumentException("Cannot parse enum from raw value: "+rawValue);}@Override@JsonValuepublicStringvalue(){returnname();}}@ValuepublicclassSimpleimplementsPlatform{Stringvalue;@Override@JsonValuepublicStringvalue(){returnvalue;}}classDeserializerextendsStdDeserializer<Platform>{protectedDeserializer(){super(Platform.class);}@OverridepublicPlatformdeserialize(JsonParserp,DeserializationContextctx)throwsIOException{returnPlatform.of(p.getValueAsString());}}}
Enter fullscreen modeExit fullscreen mode

When we parse the message withplatform we should put the deserializer accordingly.

classMessage{...@JsonDeserialize(using=Platform.Deserializer.class)privatePlatformplatform;}
Enter fullscreen modeExit fullscreen mode

Such setup us gives two opportunities. On the one hand, we can split the message flow according to theplatform value and still apply regular Java enum. Look at the code example below.

varresolvedPlatform=message.getPlatform();if(Platform.Enum.WEB.equals(resolvedPlatform)){...}elseif(Platform.Enum.MOBILE.equals(resolvedPlatform)){...}...
Enter fullscreen modeExit fullscreen mode

Besides, Jackson wrap all unexpected values withPlatform.Simple object and serialize the output result as a plain string. Meaning that the client will receive the unexpectedplatform value as-is. Look at the diagram below to clarify the point.

Jackson to the rescue

As a matter of fact, the following pattern allows us to keep using enums as a convenient language tool and also push the unexpected string values forward without data loss and pipeline termination. I think that it's brilliant.

Conclusion

Jackson is a great tool with lots of de/serialization strategies. Don't reject enum as a concept if values may vary. Look closely and see whether the library can overcome the issues.

That's all I wanted to tell you about forward compatible enum values. If you have questions or suggestions, leave your comments down below. Thanks for reading!

Resources

  1. Enum in API — The deceit of illusory simplicity by Ilya Sazonov and Fedor Sazonov
  2. Jackson library
  3. Apache Kafka
  4. RabbitMQ
  5. Apache Flink
  6. Apache Spark
  7. Kafka Streams
  8. @JsonValue
  9. @Value Lombok annotation

Top comments(5)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
vlaaaaad profile image
Lord Vlad
  • Joined
• Edited on• Edited

Pretty neat. The reason I'm choosing enums over strings is not capitalization, though. You can easily use string constants and use equalsignorecase to get around that:

public static final String WEB ="WEB";...If (WEB.equalsIgnoreCase(resolvedPlatform)) {...
Enter fullscreen modeExit fullscreen mode

Half as smelly as your initial example, and the actual check will be as concise as the check in your last snippet.

What I use enums for is switches with IDE (or other linter) exhaustiveness checks, which are not possible in your last snippet. So the enums gives you zero benefit over string constants, but introduce significant boilerplate (complexity, maintenance, yadda yadda).

Here's an idea to make it more useful:

if (resolvedPlatform instanceOf Platform.Enum platformEnum) {  switch(platformEnum) {    case WEB: ...    case MOBILE: ...    // no default, instead IDE/linter ensure exhaustiveness  }} else {  // reaolvedPlatform is a `Simple`}
Enter fullscreen modeExit fullscreen mode

Now, if you get around to addSMART_TV, but forgot to extend the switch, the linter will tell you.

Plus you get rid ofif else if else if else ....

And if you use new switch expressioncase WEB -> { ... }, it will be the compiler complaining, no linter needed.

CollapseExpand
 
kirekov profile image
Semyon Kirekov
Java team lead, conference speaker, and technical author.Telegram for contact: @kirekov
  • Location
    Russia, Moscow
  • Education
    Polzunov Altai State Technical University
  • Work
    Java team lead, a conference speaker, and a lecturer
  • Joined

@vlaaaaad thank you! Your mention about switch-expression is meaningful

CollapseExpand
 
joset98 profile image
joset98
  • Joined

This is a good one! Thanks dude

CollapseExpand
 
gigkokman profile image
Kan
  • Joined

Thank you for the great article.

shouldif (rawValue != null) { beif (rawValue == null) {?

CollapseExpand
 
kirekov profile image
Semyon Kirekov
Java team lead, conference speaker, and technical author.Telegram for contact: @kirekov
  • Location
    Russia, Moscow
  • Education
    Polzunov Altai State Technical University
  • Work
    Java team lead, a conference speaker, and a lecturer
  • Joined
• Edited on• Edited

Yeah, that's a typo. Thanks for noticing. Fixed that

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Java team lead, conference speaker, and technical author.Telegram for contact: @kirekov
  • Location
    Russia, Moscow
  • Education
    Polzunov Altai State Technical University
  • Work
    Java team lead, a conference speaker, and a lecturer
  • Joined

More fromSemyon Kirekov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp