Skip to content

Allow serialization/deserialization of enums using kebab-case. #5092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
garretwilson opened this issue Apr 12, 2025 · 19 comments
Closed

Allow serialization/deserialization of enums using kebab-case. #5092

garretwilson opened this issue Apr 12, 2025 · 19 comments
Labels
to-evaluate Issue that has been received but not yet evaluated

Comments

@garretwilson
Copy link

garretwilson commented Apr 12, 2025

Is your feature request related to a problem? Please describe.

The Java world (and many other languages) uses mostly camelCase identifiers, except for constants (in particular enums), which are identified with UPPER_SNAKE_CASE. However the modern web world uses (lowercase) kebab-case for identifiers. You can see this in:

  • URI path segments
  • URI path slugs
  • CSS classes
  • HTML values
  • etc.

Modern web frameworks such as Angular and Vue translate between the two worlds (in this case, the worlds of JavaScript/TypeScript and the web) transparently. Similarly I'm sure you're aware that JavaScript-based DOM methods use camelCase identifiers when accessing kebab-case data, both for standard attributes and actual custom data attributes.

I'm creating an entire framework that needs to convert Java enum UPPER_SNAKE_CASE to kebab-case and back transparently when serializing/deserializing.

Issue #3053 got us half the way there, but really that feature request was specific to the requestor's use case. I'm not saying that functionality is never useful. I'm saying that the usage of kebab-case (this request) in the modern web world is overwhelmingly more needed than lower_snake_case (which is effectively what #3053 produces).

Describe the solution you'd like

We need a feature to tell Jackson to serialize/deserialize an enum value such as FOO_BAR as foo-bar. Perhaps EnumFeature.WRITE_ENUMS_TO_KEBAB_CASE would work. (I assume/hope that the implementation of #3053 handles deserialization as well? I haven't tried it.)

Usage example

No response

Additional context

As I workaround, I've thrown together part of a proof of concept to override serialization. I could use some help on this so I can complete it and use it in the interim:

.addModule(new SimpleModule() //
    .addSerializer(Enum.class, new JsonSerializer<Enum>() {
      @Override
      public void serialize(Enum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(value.name().toLowerCase().replace('_', '-'));
      }
    })
    .addKeySerializer(Enum.class, new JsonSerializer<Enum>() {
      @Override
      public void serialize(Enum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeFieldName(value.name().toLowerCase().replace('_', '-'));
      }
    })

This works for serialization, although it's a shame to use raw Enum. I couldn't immediately figure out a way to prevent the warning, even using Enum<?> and the like.

However I don't see how to create a deserializer that will override the behavior for all enums. The com.fasterxml.jackson.databind.deser.std.EnumDeserializer class is huge! Is there some way to override that and do a pre-transformation on the token or something? Or is there a simpler way to add a new custom enum serializer with this functionality?

How best to add a quick-and-dirty workaround until (and if) Jackson adds this feature natively? Thanks.

@garretwilson garretwilson added the to-evaluate Issue that has been received but not yet evaluated label Apr 12, 2025
@garretwilson
Copy link
Author

garretwilson commented Apr 12, 2025

I've added Stack Overflow question Change Jackson deserialization of all enums without using annotations to help find a temporary workaround.

@JooHyukKim
Copy link
Member

Image

There are 7 naming stratgies for Enum atm. There may be other users that want different source-target naming strategy.
Would be nice if we could just pull off some global default like

mapper.customEnumNamingStrategy(
     YourCustomEnum.class
    /* source= */ UPPER_SNAKE_CASE,
    /* target= */ KEBAB_CASE
)

@garretwilson
Copy link
Author

I wasn't aware there were these existing naming strategies. Is there a way to choose one of them in my object mapper? Or are you saying that is not way at the moment?

@JooHyukKim
Copy link
Member

JooHyukKim commented Apr 12, 2025

Those Naming strategies are real. You may find them in test cases. And...

mapper.customEnumNamingStrategy(
     YourCustomEnum.class
    /* source= */ UPPER_SNAKE_CASE,
    /* target= */ KEBAB_CASE
)

... this is non-existent feature. Sorry if I confused u :-0

@garretwilson
Copy link
Author

Thanks for that information, @JooHyukKim . Do you know of a quick-and-dirty way I can add a custom enum deserializer at the the object mapper level for all enum types as a temporary workaround?

@cowtowncoder
Copy link
Member

Enum deserializers can be registered like any other, by registering Deserializers implementation, overriding this callback:

public JsonDeserializer<?> findEnumDeserializer(Class<?> type,
        DeserializationConfig config, BeanDescription beanDesc)
    throws JsonMappingException;

and registration via Module. If you have a closed set of Enums, then SimpleDeserializers via SimpleModule would also work; otherwise callback let's you create deserializer for any and all Enum types.

@cowtowncoder
Copy link
Member

cowtowncoder commented Apr 12, 2025

On EnumNamingStrategy; 2.19 will add:

    /**
     * Method for setting custom enum naming strategy to use.
     *
     * @since 2.19
     */
    public ObjectMapper setEnumNamingStrategy(EnumNamingStrategy s) {
        _serializationConfig = _serializationConfig.with(s);
        _deserializationConfig = _deserializationConfig.with(s);
        return this;
    }

but while 2.19.0-rc2 is out, final 2.19.0 not yet.
But once that is out, EnumNamingStrategies.KEBAB_CASE will be an option.

Also: @EnumNaming can be attached to Enum types via mix-ins so that's another option before 2.19.0.

@garretwilson
Copy link
Author

Well my framework is still in development, so if I can use v2.19.0-rc2 right now and call setEnumNamingStrategy(EnumNamingStrategies.KEBAB_CASE) at the object mapper level, that would be awesome! Thank you.

@cowtowncoder
Copy link
Member

Excellent! I hope it works -- being a newer feature there can be rough edges but conceptually it should work.

Plus a good way for us to get feedback

@garretwilson
Copy link
Author

For future reference it seems the naming strategies were added in issue #2667, but I didn't see where the kebab-case stuff was added.

@cowtowncoder
Copy link
Member

@garretwilson Hmmh. I think #4676 would be the issue, PR #4728.

@garretwilson
Copy link
Author

I don't see any deserialization tests of kebab-case in PR #4728. But I'll give it a spin.

@garretwilson
Copy link
Author

garretwilson commented Apr 14, 2025

I haven't tried v2.19.0-rc2 yet—I'm exploring some other avenues. But in my exploration and from looking at the code, there's a potential problem you might want to look at.

Let me ask you a question: with default enum deserialization, if Jackson is deserializing the enum value FOO_BAR and it finds Foo_Bar in the stream, will Jackson accept the value and return TestEnum.FOO_BAR? I haven't tested this, but I would guess the answer is "no", because Jackson doesn't consider values in a case-insensitive manner. And that would be the correct approach!

But what if I turn on EnumNamingStrategies.KEBAB_CASE? What if Foo-Bar is in the stream? Would Jackson blindly change it to uppercase FOO_BAR before trying to create the enum instance, ignoring the fact that the value should have been foo-bar instead of Foo-Bar? If so, EnumNamingStrategies.KEBAB_CASE in effect would change Jackson behavior to be case-insensitive!! (One easy but perhaps not the most efficient way to guard against this would be, after deserialization, to do the reverse transformation from the enum name and verify that the original value matched what was expected.)

But I don't even see any deserialization code for kebab-case enums in PR #4728, or any tests, so I'm guessing about behavior you might want to check for.

@cowtowncoder
Copy link
Member

I did not implement this feature so I don't know off-hand, but one thing to note on all Naming Strategies -- they work statically, mapping class defined names, without considering possible input. So it's all about going from implied name (from Enum constant) to expected/produced output name; and there can only be a single mapping for each Enum constant.

From that, incoming String is never changed to anything; but it is expected to match "external name" produced from Enum name.

@lbenedetto Might be better able to answer here, having provided the PR.

@garretwilson
Copy link
Author

… mapping class defined names, without considering possible input.

Oh, I see—so they don't do a reverse transformation on the serialized value, but rather transform the expected enum values and compare those to what the input is? OK, then that would keep case sensitivity, and would also explain why I didn't see any reverse transformation code.

Thanks for the explanation.

@cowtowncoder
Copy link
Member

Yes, exactly.

@cowtowncoder
Copy link
Member

@garretwilson Can this be closed?

@cowtowncoder cowtowncoder changed the title Allow serializion/deserialization of enums using kebab-case. Allow serialization/deserialization of enums using kebab-case. Apr 23, 2025
@garretwilson
Copy link
Author

@cowtowncoder thanks for checking back. Yes, I think the EnumNamingStrategies.KEBAB_CASE setting will perform essentially what I was requesting in this ticket; I'll go ahead and close it.

Note however that in the end I opted to add serialization/deserialization manually using serialization and deserialization modifiers, which I discovered from an answer to my question Change Jackson deserialization of all enums without using annotations on Stack Overflow. The reason is that I'm creating a framework and I'd like all the enums to use kebab-case by default for worry-free and modern marshalling of values. However I want the framework to be flexible enough that if an expert developer wants to use Jackson annotations such as @JsonFormat or @JsonValue to customize the serialization of a particular enum type, the framework will let them.

So in a future improvement to my framework, I'm going to add checks for Jackson annotations on each serialized type to the serializer/deserializer modifiers, and if any such annotations are present I'll just delegate to the default serializers/deserializers. (This is the major benefit for me of the modifiers—they provide access to the default serializer/deserializer.) If you're interested you can read more details on my ticket FLANGE-30.

As always thanks for your attentiveness and communication.

@cowtowncoder
Copy link
Member

@garretwilson Makes total sense & thank you for sharing your situation, context and solutions. Modifiers are indeed powerful and flexible ways to augment (or replace) existing handling; glad you found them and managed to get things working the way you want!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to-evaluate Issue that has been received but not yet evaluated
Projects
None yet
Development

No branches or pull requests

3 participants