Overcome "Can not construct instance of InterfaceClass" without hinting the parent

5.4k Views Asked by At

I have this method in my controller:

@RequestMapping(method = RequestMethod.POST)
InterfaceClass insert(@RequestBody InterfaceClass interfaceClass) {

    // Do something
}

The error I am getting is pretty straightforward and self-explanatory:

Can not construct instance of InterfaceClass: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information.

Basically, I need to tell Spring that I have a concrete implementation of InterfaceClass, ClassImpl.

I tried:

@JsonRootName("InterfaceClass")
public class ClassImpl implements InterfaceClass {
}

to no extent. I cannot use @JsonTypeInfo as the parent interface class InterfaceClass should not be aware of ClassImpl and they are in different modules. What I have also tried:

Implement InterfaceClass with abstract AbstractClass and put:

@JsonDeserialize(as = AbstractClass.class)

on top of InterfaceClass. Then extend AbstractClass with ClassImpl. The error simply becomes:

Can not construct instance of InterfaceClass: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information.

Further tried:

public class ControllerClass<E extends InterfaceClass> {

    @RequestMapping(method = RequestMethod.POST)
    InterfaceClass insert(@RequestBody E interfaceClass) {
        InterfaceClass object = (InterfaceClass) interfaceClass;
    }
}

and this results in:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to InterfaceClass

as expected.

I was really expecting Spring Boot to handle component discovery since there is only one concrete implementation of InterfaceClass or AbstractClass, which is ClassImpl in my classpath. Maybe I am doing something wrong? How can I overcome this, without explicitly hinting the InterfaceClass on where its implementation is (e.g. no @JsonDeserialize, etc.)?

1

There are 1 best solutions below

0
On BEST ANSWER

Solution 1 - Dynamically register subtypes

You can define the subtypes dynamically.

1. On the interface, define a JSON field (@type) to be used as identifier:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, 
              include = JsonTypeInfo.As.PROPERTY, property = "@type")
public interface InterfaceClass {
}

2. Add "@type" field to your JSON payload

{
    ...
    "@type": "someName"
}

2. Register the inteface's subtypes dynamically:

@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() {
        public void configure(ObjectMapper objectMapper) {
            objectMapper.registerSubtypes(ClassImpl.class);
            super.configure(objectMapper);
        };
    };
    return builder;
}

4. Specify the "@type" name on your concrete class (optional):

//Optional, otherwise uses the Simple class name (ie: 'ClassImpl')
@JsonTypeName("someName") 
public class ClassImpl implements InterfaceClass {
}

5. Use can now use the interface with @RequestBody:

@RequestMapping(method = RequestMethod.POST)
InterfaceClass insert(@RequestBody InterfaceClass interfaceClass) {
}

Solution 2 - Dynamically register a custom Deserializer

If adding a @type field isn't possible (or not wanted), you may also define a Custom Deserializer for your interface, which would in fact create a ClassImpl:

1. Define a custom deserializer:

class ClassImplJsonDeserializer extends JsonDeserializer<ClassImpl> {
    @Override
    public ClassImpl deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return jp.readValuesAs(ClassImpl.class).next();
    }
}

2. Dynamically set the custom deserializer:

@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.deserializerByType(InterfaceClass.class, new ClassImplJsonDeserializer());
    return builder;
}

3. Remove @JsonTypeInfo from the interface:

public interface InterfaceClass {
}