Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to automatically map @DefaultValue to an enum parameter using JAX-RS based Restlet?

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...?

like image 533
Sophie Avatar asked Feb 03 '13 12:02

Sophie


1 Answers

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:

  1. Be a primitive type.
  2. Have a constructor that accepts a single String argument.
  3. Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String)).
  4. Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
  5. Be 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.

The solution...

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!

like image 107
jeffwtribble Avatar answered Oct 16 '22 17:10

jeffwtribble