1
\$\begingroup\$

I have several enums that all implement this interface:

public interface SelectOption {
    String getId();
    String getLabel();
}

Here's an example:

public enum AuthType implements SelectOption {
    BASIC("basic", "Basic Auth"),
    OAUTH("oauth", "OAuth"),
    TOKEN("token", "Bearer Token");

    private final String id;
    private final String label;

    private AuthType(String id, String label) {
        this.id = id;
        this.label = label;
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public String getLabel() {
        return this.label;
    }
}

I receive this enum from the API client. First, the client asks for available select options (in this case it's the authentication type), so it needs both the id (the one to refer internally and in JSON that goes to the backend) and label (the user-friendly name to display in the UI). I want to use a snake case (bla_bla) or kebab case (bla-bla) notation because I feel it's more natural for any API and so that it does not depend on my java enum constants so that I can rename them as I like. The actual use-case is that the UI will display a drop-down of available authentication methods, the user will pick one and fill out method-specific parameter, i.e. for BASIC, that'd be username and password, for TOKEN, that'd be the token.

The default deserialization behavior is to match up enum constant names, i.e. BASIC string would match with AuthType#BASIC automatically. I'm using Jackson for deserialization.

Every time I get an enum that implements SelectOption in a serialized form (as a String), I get it as id that is equal to SelectOption#getId(). So, if I supply basic, it would error saying something like "basic" not found in enumeration "AuthType", allowed values are: "BASIC", "OAUTH", "TOKEN". And for every other enum, I get it as the enum constant name, so the default deserialization works.

I couldn't create a JsonDeserializer for SelectOption, because there is a built-in EnumDeserializer that takes precedence, so I decided to register a deserializer that works with all enums (registered to ObjectMapper for Enum.class) and delegates to EnumDeserializer if it's not a SelectOption.

The following solution works but I'm not sure if it's the best way to do it:

public class SelectOptionDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer {
    JavaType type;

    public SelectOptionDeserializer() {
    }

    public SelectOptionDeserializer(JavaType type) {
        this.type = type;
    }

    @Override
    public Enum<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (!SelectOption.class.isAssignableFrom(type.getRawClass())) {
            EnumDeserializer enumDeserializer = new EnumDeserializer(EnumResolver.constructFor(ctxt.getConfig(), type.getRawClass()), false);
            return (Enum<?>) enumDeserializer.deserialize(p, ctxt);
        }
        String id = p.getText();
        return (Enum<?>) Stream.of(type.getRawClass().getEnumConstants())
                .map(SelectOption.class::cast)
                .filter(enumConstant -> enumConstant.getId().equals(id))
                .findFirst()
                .orElse(null);
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        return new SelectOptionDeserializer(ctxt.getContextualType() != null
                ? ctxt.getContextualType()
                : property.getMember().getType());
    }
}
\$\endgroup\$
5
  • 2
    \$\begingroup\$ Is there a good reason for having the ids different to the names of the enum values? \$\endgroup\$
    – tgdavies
    Jan 19, 2022 at 5:09
  • 2
    \$\begingroup\$ I really don't understand what you want to achieve. Can you please add a bit about that? \$\endgroup\$ Jan 19, 2022 at 15:43
  • \$\begingroup\$ @RobAudenaerde Thanks! I added another paragraph to the description \$\endgroup\$
    – Sam
    Jan 19, 2022 at 20:34
  • \$\begingroup\$ @tgdavies Thanks! I added another paragraph to the description that explains the reason \$\endgroup\$
    – Sam
    Jan 19, 2022 at 20:34
  • 1
    \$\begingroup\$ Actually, your question pushed me to think if my reasoning is good enough. Maybe I should just use the enum constant's name... \$\endgroup\$
    – Sam
    Jan 19, 2022 at 20:40

1 Answer 1

1
\$\begingroup\$

As per the discussion in the comments, I decided to simply use Enum#name() instead of SelectOption#getId(). I'm already using a lot of other enums in my JSON responses even though those are simple "type" enums without any extra data like label here but nonetheless, I think I was wrong about screaming snake case (BLA_BLA) not fitting into APIs, they fit just fine.

Final solution:

public interface SelectOption {
    /**
      * refers to {@link Enum#name()}
      */
    String name();
    String getLabel();
}

The actual enum:

public enum AuthType implements SelectOption {
    BASIC("Basic Auth"),
    OAUTH("OAuth"),
    TOKEN("Bearer Token");

    private final String label;

    private AuthType(String label) {
        this.label = label;
    }

    @Override
    public String getLabel() {
        return this.label;
    }
}

This way the default deserialization works as needed, so, no need for a deserializer class.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your solution! I'm struggling with the same issue, I have an interface and multiple enums implementing the interface. I found that adding a name() method to the interface was not sufficient. What else did you do? Thanks in advance! \$\endgroup\$ May 3, 2023 at 21:13
  • \$\begingroup\$ jackson can deserialize enums out of the box. "BASIC" would become AuthType#BASIC. You don't even need that name() method on the interface unless you're using polymorphism somewhere yourself \$\endgroup\$
    – Sam
    May 5, 2023 at 7:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.