Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring generic REST controller: parsing request body

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.

like image 478
Oleg Gumennyj Avatar asked Nov 11 '15 21:11

Oleg Gumennyj


People also ask

Is @RequestBody required with @RestController?

If you don't add @RequestBody it will insert null values (should use), no need to use @ResponseBody since it's part of @RestController.

How do I request body data in Spring?

Simply put, the @RequestBody annotation maps the HttpRequest body to a transfer or domain object, enabling automatic deserialization of the inbound HttpRequest body onto a Java object. Spring automatically deserializes the JSON into a Java type, assuming an appropriate one is specified.

Can we use @RequestBody in Spring MVC?

The @RequestBody annotation is applicable to handler methods of Spring controllers. This annotation indicates that Spring should deserialize a request body into an object. This object is passed as a handler method parameter.

What is @RequestBody in REST API?

A request body is data sent by the client to your API. A response body is the data your API sends to the client. Your API almost always has to send a response body.


2 Answers

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)

like image 168
user158037 Avatar answered Nov 03 '22 13:11

user158037


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.

like image 36
ahll Avatar answered Nov 03 '22 14:11

ahll