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());
}
}