6

I have WebDeviceInfo and IOSDeviceInfo classes that are subclasses of DeviceInfo. How can I create a single endpoint in a Spring @RestController that will accept either IOSDeviceInfo or WebDeviceInfo?

Attempt #1

I tried to map the same RequestMapping to two different methods, one that would get called if the RequestBody could be mapped to a WebDeviceInfo and the other that would get called if the RequestBody could be mapped to a IOSDeviceInfo.

@RequestMapping(value = "/register-device", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public void registerWebDevice(@RequestBody final WebDeviceInfo webDeviceInfo) {
    //register web device
}

@RequestMapping(value = "/register-device", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public void registerIOSDevice(@RequestBody final IOSDeviceInfo iosDeviceInfo) {
    //register ios device
}

But this does not work, the second RequestMapping does not get registered and the application fails to start up because Spring sees that /register-device with the same RequestMethod and MediaType is already mapped to another method.

Attempt #2

Next, I tried accepting the superclass as the RequestBody and then casting it to the appropriate subclass.

@RequestMapping(value = "/register-device", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public void registerDevice(@RequestBody final DeviceInfo deviceInfo) {
    if (deviceInfo instanceof WebDeviceInfo) {
        final WebDeviceInfo webDeviceInfo = (WebDeviceInfo) deviceInfo;
        //register web device
    } else if (deviceInfo instanceof IOSDeviceInfo) {
        final IOSDeviceInfo iosDeviceInfo = (IOSDeviceInfo) deviceInfo;
        //register ios device
    } else {
        logger.debug("Could not cast deviceInfo to WebDeviceInfo or IOSDeviceInfo");
    }
}

This does not work either. I always get:

Could not cast deviceInfo to WebDeviceInfo or IOSDeviceInfo

Attempt #3

Finally, I tried just casting to the correct subclass inside a try/catch.

@RequestMapping(value = "/register-device", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public void registerDevice(@RequestBody final DeviceInfo deviceInfo) {
    try {
        final WebDeviceInfo webDeviceInfo = (WebDeviceInfo) deviceInfo);
        //register web device
    } catch (final ClassCastException ex) {
        try {
            final IOSDeviceInfo iosDeviceInfo = (IOSDeviceInfo) deviceInfo);
            //register ios device
        } catch (final ClassCastException ex2) {
            logger.debug("Could not cast deviceInfo to WebDeviceInfo or IOSDeviceInfo");
        }
    }
}

Again I get error:

Could not cast deviceInfo to WebDeviceInfo or IOSDeviceInfo

Is there any way to accomplish this, or am I going to have to create two separate methods with two different RequestMappings?

Roman C
  • 49,761
  • 33
  • 66
  • 176
Andrew Mairose
  • 10,615
  • 12
  • 60
  • 102
  • it doesn't work this way, you need to extend your httpMssagerConverter to `deserialize` the http request, which should be very easy, if you are using `jackson` – Jaiwo99 May 18 '17 at 15:48
  • 1
    For your second and third attempts, how should Spring MVC decide to deserialize the content as either an `IOSDeviceInfo` or a `WebDeviceInfo`? – Sotirios Delimanolis May 18 '17 at 15:52
  • @Jaiwo99 do you have any example of how to override the default one that Spring provides? – Andrew Mairose May 18 '17 at 16:02
  • how about accepting your RequestBody as a String and deserialise the json accordingly? you might need a flag to indicate whether it is IOSDeviceInfo or WebDeviceInfo, preferably a PathVariable – Drunken Daddy May 18 '17 at 16:08

3 Answers3

2

Attempts #2 and #3 should work when you annotate the base class DeviceInfo with the correct Jackson annotations:

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = IOSDeviceInfo.class, name = "ios"),
        @JsonSubTypes.Type(value = WebDeviceInfo.class, name = "web")
})
public abstract class DeviceInfo {
    [...]
}

class IOSDeviceInfo extends DeviceInfo {
    [...]
}

class WebDeviceInfo extends DeviceInfo {
    [...]
}

Then when you receive a request, the body will be deserialized into the correct subclass, either a IOSDeviceInfo or a WebDeviceInfo, depending on the 'type' parameter in the JSON body:

{
  type : "ios",
  [...]
}

Now you only need a single @RequestMapping method:

@RequestMapping(value = "/register-device", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public void registerDevice(@RequestBody final DeviceInfo deviceInfo) {
    if (deviceInfo instanceof WebDeviceInfo) {
        final WebDeviceInfo webDeviceInfo = (WebDeviceInfo) deviceInfo;
        //register web device
    } else if (deviceInfo instanceof IOSDeviceInfo) {
        final IOSDeviceInfo iosDeviceInfo = (IOSDeviceInfo) deviceInfo;
        //register ios device
    } else {
        logger.debug("Could not cast deviceInfo to WebDeviceInfo or IOSDeviceInfo");
    }
}
GeertPt
  • 16,398
  • 2
  • 37
  • 61
0

You can use @PathVariable to the parameter to define which type you need to deserialize

public void registerDevice(@PathVariable("deviceType") String deviceType, @RequestBody final DeviceInfo deviceInfo) {

switch (deviceType){
...
}
Roman C
  • 49,761
  • 33
  • 66
  • 176
-1

You can use the same @RequestMapping, however, you still need something to differentiate them. By same, I mean the same name, path, and RequestMethod.

You can use the params argument in request mapping to differentiate them.

For example,

@RequestMapping(value = "/register-device", 
    method = RequestMethod.POST, 
    consumes = MediaType.APPLICATION_JSON_VALUE, 
    params="device=android")
public void registerDevice(@RequestBody final WebDeviceInfo deviceInfo){

}


@RequestMapping(value = "/register-device", 
    method = RequestMethod.POST, 
    consumes = MediaType.APPLICATION_JSON_VALUE,    
    params="device=ios")
public void registerDevice(@RequestBody final IOSDeviceInfo deviceInfo){

}

Then just add an extra parameter to the request with the key "device", and Spring will resolve the correct method.

e.g., if this were your original payload:

{
   deviceInfo : {..}
}

it would now be this:

{
    device : "ios",
    deviceInfo : {..}
}
Christopher Schneider
  • 3,745
  • 2
  • 24
  • 38
  • Unfortunately, this doesn't work for a value in the JSON payload. The `device` here would have to be be a request parameter, rather than a value in the POSTed JSON. This would require switching the URL to `/register-device?device=ios` or `/register-device?device=web`. – Andrew Mairose May 25 '17 at 18:30