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
.
If you don't add @RequestBody it will insert null values (should use), no need to use @ResponseBody since it's part of @RestController.
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.
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.
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.
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With