Spring generic REST controller: parsing request body

23,544

Solution 1

For a long time research, I found that works in Jackson:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
public interface ApiRequest {

}

and use

REQUEST extends ApiRequest 

with this, do not change MessageConverter. Thus it will still require extra class info in your json request. For example, you could do:

public abstract class ApiPost<REQUEST extends ApiRequest > {

    abstract protected Response post(REQUEST request) throws ErrorException;

    @ResponseBody
    @RequestMapping(method = RequestMethod.POST)
    public Response post(
            @RequestBody REQUEST request
    ) throws IOException {

        return  this.post(request);
    }

}

and then for controller

 public class ExistApi {

        public final static String URL = "/user/exist";

        @Getter
        @Setter
        public static class Request implements ApiRequest{

            private String username;
        }

    }
@Controller
@RequestMapping(URL)
public class ExistApiController extends ApiPost<Request> {

    @Override
    protected Response post(Request request) implements ApiRequest  {
       //do something
       // and return response
    }

}

And then send request as { "username":xxxx, "@class":"package.....Request" }

reference a https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization

But for me, the best solution is not use spring to convert message, and left the abstract class do it.

public abstract class ApiPost<REQUEST> {

    @Autowired
    private ObjectMapper mapper;

    protected Class<REQUEST> getClazz() {
        return (Class<REQUEST>) GenericTypeResolver
                .resolveTypeArgument(getClass(), ApiPost.class);
    }

    abstract protected Response post(REQUEST request) throws ErrorException;

    @ResponseBody
    @RequestMapping(method = RequestMethod.POST)
    public Response post(
            @RequestBody REQUEST request
    ) throws IOException {

        //resolve spring generic problem
        REQUEST req = mapper.convertValue(request, getClazz());
        return  this.post(request);
    }

}

with this, we do not require the ApiRequest interface and @class in request json, decouple the front and backend.

Pardon my poor english.

Solution 2

What Spring really sees is:

public class CrudController {

    @RequestMapping(method = GET)
    public Iterable<Object> findAll(@PathVariable String entity) {
    }

    @RequestMapping(value = "{id}", method = GET)
    public Object findOne(@PathVariable String entity, @PathVariable String id)     {
    }

    @RequestMapping(method = POST)
    public void save(@PathVariable String entity, @RequestBody Object body) {
    }
}

For returned objects it doesn't matter as Jackson will generate output proper JSON anyway but it looks like Spring can't handle incoming Object same way.

You might try replacing generics with just SomeSuperEntity and take a look at Spring @RequestBody containing a list of different types (but same interface)

Share:
23,544
Oleg  Gumennyj
Author by

Oleg Gumennyj

Updated on December 21, 2020

Comments

  • Oleg  Gumennyj
    Oleg Gumennyj over 3 years

    I have following controller:

    @RestController
    @RequestMapping(value = "/{entity}", produces = MediaType.APPLICATION_JSON_VALUE)
    public class CrudController<T extends SomeSuperEntity> {
    
        @RequestMapping(method = GET)
        public Iterable<T> findAll(@PathVariable String entity) {
        }
    
        @RequestMapping(value = "{id}", method = GET)
        public T findOne(@PathVariable String entity, @PathVariable String id) {
        }
    
        @RequestMapping(method = POST)
        public void save(@PathVariable String entity, @RequestBody T body) {
        }
    }
    

    SomeSuperEntity class looks like:

    public abstract class SomeSuperEntity extends AbstractEntity {
        // some logic
    }
    

    And AbstractEntity its abstract class with some field:

    public abstract class AbstractEntity implements Comparable<AbstractEntity>, Serializable {
    
        private Timestamp firstField;
        private String secondField;
    
        public Timestamp getFirstField() {
            return firstField;
        }
    
        public void setFirstField(Timestamp firstField) {
            this.firstField = firstField;
        }
    
        public String getSecondField() {
            return secondField;
        }
    
        public void setSecondField(String secondField) {
            this.secondField = secondField;
        }
    }
    

    All subclasses of SomeSuperEntity - simple JavaBeans. In case with findAll() and findOne(id) methods - everything works fine. I create entity in service layer and it returns to client as JSON with all fields that declared in subclass and in AbstractEntity.

    But when i tried to get request body in save(entity, body), i got following error:

    Could not read document: Can not construct instance of SomeSuperEntity, problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information

    If i remove abstract from SomeSuperEntity, everything works, but i request body i got only those fields, that declared in AbstractEntity.

    And here is my question, is there any workaround for such problems in my case? If not, what would by the best solution here without any structure changes (making subcontloller for each entity is not an option)? Is retrieve body as plain text would be a good idea? Or it would be better to use Map for this?

    I'm using Spring v4.2.1 and Jackson 2.6.3 as converter.

    There is a bit of information about generic controllers, but i couldn't find anything that cover my case. So, please, navigate in case of duplicate question.

    Thanks in advance.

    UPD: Currently its working as follow: I add add additional check in my MessageConverter and define @RequestBody as String

    @Override
        public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
            if (IGenericController.class.isAssignableFrom(contextClass)) {
                return CharStreams.toString(new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders())));
            }
            return super.read(type, contextClass, inputMessage);
        }
    

    Then, on service layer, i define what entity received (in plain json) and convert it:

    final EntityMetaData entityMetadata = getEntityMetadataByName(alias);
    final T parsedEntity = getGlobalGson().fromJson(entity, entityMetadata.getEntityType());
    

    Where EntityMetaData is enum with defined relations between entity alias and class. Alias comes as @PathVariable.

  • dukethrash
    dukethrash over 6 years
    Where do you use T extends ApiRequest? Can you give an example?