I have a web API where the user may (or may not) transfer an URL parameter like for example bird
, dog
etc.
I want this parameter to be mapped to an enum on the server side, something like:
@POST
@Path("/zoo")
public Response createNewAnimal(
@QueryParam("animal")
@DefaultValue("CAT") AnimalType type
) throws Exception
...
public enum AnimalType {
BIG_BIRD,
SMALL_CAT;
}
But it doesn't work!
While processing the web request, Enum.valueOf()
is being called. And of course it fails, because the bird
that user uses as URL parameter doesn't match the identifier in the Enum
(AnimalType.BIG_BIRD
).
There is no way to override to valueOf()
method (it's static...) and setting constructor doesn't help (it's the opposite logical direction).
So maybe you know of a nice solution to this, instead of just using if...else...?
The behavior of enum (de)serialization with JAX-RS and Jackson 2.5.0 tripped me up for a while, so I'm going to try and elaborate on @Bogdan's answer, and show what worked for me.
The thing that wasn't clear to me was that @QueryParam
and @FormParam
don't follow standard procedure to deserialize enums - so if you're trying to accept an enum as a query param, like so:
@GET
public Response getAnimals(@QueryParam("animalType") AnimalType animalType) {}
...then the only way your animalType
argument will be deserialized properly is if your type T
(in our case, AnimalType
) satisfies one of the following properties:
String
argument.valueOf
or fromString
that accepts a
single String
argument (see, for example, Integer.valueOf(String)
).ParamConverterProvider
JAX-RS
extension SPI that returns a ParamConverter
instance capable of a
"from string" conversion for the type.List<T>
, Set<T>
or SortedSet<T>
, where T
satisfies 2, 3 or 4
above. The resulting collection is read-only....per the Java EE 7 @QueryParam docs.
This means that, in addition to implementing custom (de)serialization for your normal use cases, you will also need to satisfy one of the five conditions listed above. Then, and only then!, you'll be able to handle the @QueryParam
deserialization case.
A simple way that I found to handle both the normal (de)serialization cases and the @QueryParam
case is to a) satisfy condition #3 by implementing fromString()
, and b) implement a mapper class that contains both a serializer and a deserializer, the latter of which will rely on fromString()
, so we have consistent deserialization:
// Our example enum class...
@JsonSerialize(using = AnimalTypeMapper.Serializer.class)
@JsonDeserialize(using = AnimalTypeMapper.Deserializer.class)
public enum AnimalType {
CAT("cat"),
BIRD("bird"),
DOG("doggy");
private final String name;
AnimalType(String name) {
this.name = name;
}
private static Map<String, AnimalType> VALUES_BY_NAME = Arrays.stream(values())
.collect(Collectors.toMap(AnimalType::getName, Function.identity()));
public String getName() {
return name;
}
// Implementing this method allows us to accept AnimalType's as @QueryParam
// and @FormParam arguments. It's also used in our custom deserializer.
public static AnimalType fromString(String name) {
return VALUES_BY_NAME.getOrDefault(name, DOG);
}
}
// Our custom (de)serialization class...
public class AnimalTypeMapper {
public static class Serializer extends JsonSerializer<AnimalType> {
@Override
public void serialize(AnimalType animalType, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(animalType.getName());
}
}
public static class Deserializer extends JsonDeserializer<AnimalType> {
@Override
public AnimalType deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
return AnimalType.fromString(jsonParser.getValueAsString());
}
}
}
Hopefully someone out there will find this helpful. I spent way too much time spinning my wheels on this!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With