I would like to be able to convert a generic Object in a request to a custom object that I have built using Spring Boot and Jackson.
For example, say I have the following controller method:
#RequestMapping(value = "/user/action", method = RequestMethod.POST)
public String processAction(#RequestBody Request theRequest){
return "Success";
}
Now the Request object looks like this:
public class Request {
public Request(){
}
private String action;
private Object actionRequest;
//getters/setters
The actual actionRequest can be any Object (e.g. Boolean, String, or a custom built one).
So say I have a custom built ActionA class:
public class ActionA {
public ActionA(){
}
private int actionAInfo;
//getters/setters
}
If I invoke my controller with a POST and a payload of
{"action": "startEstimate", "actionRequest":true}
when it reaches the method, the 'actionRequest' is already converted to a boolean.
However, if I provide the payload
{"action": "startEstimate", "actionRequest": {"actionAInfo": 5}}
the 'actionRequest' is just converted to a HashMap with key of 'actionAInfo' and value of '5'.
How can I configure Spring Boot/Jackson to create the ActionA object?
One workaround I have seen is to instead of having Object in the Request, use ObjectNode. Then do something similar to
ObjectMapper om = new ObjectMapper();
ActionA actionA = om.convertValue(theRequest.getActionRequest(), ActionA.class);
However, this does not expand as I add more actions because I would need to know the type before I attempt to build it.
I have also attempted a solution presented here
Custom JSON Deserialization with Jackson
and it does not seem to be working. I have created a new ActionADeserializer
public class ActionADeserializer extends JsonDeserializer<ActionA> {
#Override
public ActionA deserialize(JsonParser jp, DeserializationContext ctxt) throws
IOException,
JsonProcessingException {
return jp.readValueAs(ActionA.class);
}
}
and altered ActionA class to have
#JsonDeserialize(using = ActionADeserializer.class)
public class ActionA
When I submit the payload
{"action": "startEstimate", "actionRequest": {"actionAInfo": 5}}
it still creates the HashMap instead of the ActionA object. I set a breakpoint in the deserializer and do not even see it being invoked.
Related
I am currently trying to develop a solution within Quarkus that can serialize JSON responses dynamically based on the properties (i.e. query params, headers, etc.) of the associated request. Here's an example of what I am trying to achieve.
The user sends a GET request containing a query param lang which specifies the language.
#GET()
#Produces(MediaType.APPLICATION_JSON)
public Content getContent(
#QueryParam("lang")
#DefaultValue("en")
Language language
) {
// Fetch and return content from service
}
The Content class contains fields of type TranslatableString, which store keys used for lookups.
public #Value class TranslatableString {
String key;
}
The associated JsonSerializer should now take this custom type and use its key to provide the translation for the requested language.
#ApplicationScoped
public class Serializer extends JsonSerializer<TranslatableString> {
#Inject
TranslationStore translations;
#Override
public void serialize(TranslatableString value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(
translations.get(value.getKey(), /** ??-Language-?? **/ )
);
}
}
Is there some (easy) way to access the user-provided query parameters inside the Jackson JsonSerializer?
EDIT
This is how I register my custom serializer.
#Singleton
public class ObjectMapperConfig implements ObjectMapperCustomizer {
#Inject
Serializer serializer;
#Override
public void customize(ObjectMapper objectMapper) {
objectMapper.registerModule(
new SimpleModule()
.addSerializer(
TranslatableString.class,
serializer
)
);
}
}
Based on #RobSpoor's comment, I came up with the following solution using MapStruct and a DTO instead of relying on Jackson to do the translation.
Create domain specific and DTO class.
public #Value class Content {
TranslatableString body;
}
// Instances of Content will be mapped to ContentDto by using MapStruct
public #Value class ContentDto {
String body;
}
Create MapStruct mapper for mapping from domain object to DTO. Use MapStruct's Context feature to pass in language during mapping.
#Mapper(componentModel = "cdi")
public abstract class ContentMapper {
// Dummy translations
private final Map<Language, Map<String, String>> translations = Map.of(
Language.ENGLISH, Map.of(
"greeting", "Hello World"
),
Language.GERMAN, Map.of(
"greeting", "Hallo Welt"
)
);
// The language parameter will be passed to the translate method
abstract ContentDto toDto(Content content, #Context Language language);
// Use the language parameter to retrieve the right translation
protected String translate(TranslatableString translatableString, #Context Language language) {
return translations.get(language).get(translatableString.getKey());
}
}
Pass language into mapper during service call.
#Path("/greeting")
public class GreetingResource {
#Inject
ContentMapper mapper;
#GET
#Produces(MediaType.APPLICATION_JSON)
public ContentDto evaluate(
#QueryParam("lang")
#DefaultValue("en")
Language language
) {
return mapper.toDto(
new Content(new TranslatableString("greeting")),
language
);
}
}
Now /greeting can be called with an extra query parameter of lang specifying the desired language.
Given the following basic domain model:
abstract class BaseData { ... }
class DataA extends BaseData { ... }
class DataB extends BaseData { ... }
I want to write a Spring MVC controller endpoint thus ...
#PostMapping(path="/{typeOfData}", ...)
ResponseEntity<Void> postData(#RequestBody BaseData baseData) { ... }
The required concrete type of baseData can be inferred from the typeOfData in the path.
This allows me to have a single method that can handle multiple URLs with different body payloads. I would have a concrete type for each payload but I don't want to have to create multiple controller methods that all do the same thing (albeit each would do very little).
The challenge that I am facing is how to "inform" the deserialization process so that the correct concrete type is instantiated.
I can think of two ways to do this.
First use a custom HttpMessageConverter ...
#Bean
HttpMessageConverter httpMessageConverter() {
return new MappingJackson2HttpMessageConverter() {
#Override
public Object read(final Type type, final Class<?> contextClass, final HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// TODO How can I set this dynamically ?
final Type subType = DataA.class;
return super.read(subType, contextClass, inputMessage);
}
};
}
... which gives me the challenge to determine the subType based on the HttpInputMessage. Possibly I could use a Filter to set a custom header earlier when the URL is available to me, or I could use a ThreadLocal also set via a Filter. Neither sounds ideal to me.
My second approach would be to again use a Filter and this time wrap the incoming payload in an outer object which would then provide the type in a way that enables Jackson to do the work via #JsonTypeInfo. At the moment this is probably my preferred approach.
I have investigated HandlerMethodArgumentResolver but if I try to register a custom one it is registered AFTER the RequestResponseBodyMethodProcessor and that class takes priority.
Hmm, so after typing all of that out I had a quick check of something in the RequestResponseBodyMethodProcessor before posting the question and found another avenue to explore, which worked neatly.
Excuse the #Configuration / #RestController / WebMvcConfigurer mash-up and public fields, all for brevity. Here's what worked for me and achieved exactly what I wanted:
#Configuration
#RestController
#RequestMapping("/dummy")
public class DummyController implements WebMvcConfigurer {
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#interface BaseData {}
public static class AbstractBaseData {}
public static class DataA extends AbstractBaseData {
public String a;
}
public static class DataB extends AbstractBaseData {
public String b;
}
private final MappingJackson2HttpMessageConverter converter;
DummyController(final MappingJackson2HttpMessageConverter converter) {
this.converter = converter;
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(
new RequestResponseBodyMethodProcessor(Collections.singletonList(converter)) {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BaseData.class)
&& parameter.getParameterType() == AbstractBaseData.class;
}
#Override
protected <T> Object readWithMessageConverters(
NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
throws IOException, HttpMediaTypeNotSupportedException,
HttpMessageNotReadableException {
final String uri =
webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI();
return super.readWithMessageConverters(
webRequest, parameter, determineActualType(webRequest, uri));
}
private Type determineActualType(NativeWebRequest webRequest, String uri) {
if (uri.endsWith("data-a")) {
return DataA.class;
} else if (uri.endsWith("data-b")) {
return DataB.class;
}
throw new HttpMessageNotReadableException(
"Unable to determine actual type for request URI",
new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class)));
}
});
}
#PostMapping(
path = "/{type}",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<? extends AbstractBaseData> post(#BaseData AbstractBaseData baseData) {
return ResponseEntity.ok(baseData);
}
}
The key to this is that I stopped using #RequestBody because that is what was preventing me overriding the built-in behaviour. By using #BaseData instead I get a HandlerMethodArgumentResolver that uniquely supports the parameter.
Other than that it was a case of assembling the two objects that already did what I needed, so autowire a MappingJackson2HttpMessageConverter and instantiate a RequestResponseBodyMethodProcessor with that one converter. Then pick the right method to override so that I could control what parameter type was used at a point that I had access to the URI.
Quick test. Given the following payload for both requests ...
{
"a": "A",
"b": "B"
}
POST http://localhost:8081/dummy/data-a
... gives a response of ...
{
"a": "A"
}
POST http://localhost:8081/dummy/data-b
... gives a response of ...
{
"b": "B"
}
In our real-world example this means that we will be able to write one method each that supports the POST / PUT. We need to build the objects and configure the validation possibly - or alternatively if we use OpenAPI 3.0 which we are investigating we could generate the model and validate without writing any further code ... but that's a separate task ;)
I am invoking a REST POST API with following body:
{
"ref":{" ":"123"}
}
In the backend I am using Object Mapper to deserialize the above body to a POJO object as shown below -
public class POJO{
public Map<String,String> ref;
public Map<String,String> getRef(){
return ref;
}
public void setRef(Map<String,String> map){
this.ref = map;
}
}
But I want to throw an exception when whitespace key is sent as in the above case. Currently Object mapper allows the above behavior.
I don't want to use a custom deserializer.
Is there a way using jackson annotation or using some available module which can be registered in Object mapper so that it doesn't allows the above body and throws an exception?
What I'd like to do seems simple, just handling an object like the following:
When receiving a Post request from a client(web browser), which has a JSON object in its body, the server deserializes it into a certain object, and serializes some of its fields then saves them into a database.
When receiving a Get request from the client, the server retrieves the object from the database, deserializes it, composes it with other information and gives it back to the client.
The problem is, the object contains an abstract field.
Saving a request works fine
When I add these annotations to the abstract class, the phase 1 works fine.
Request JSON:
{ config: { type: "concreteA", ...}, ...}
REST API:
#RequestMapping(value="/configs", method=RequestMethod.POST)
#ResponseBody
public ResponseEntity<Object> saveConfig(#RequestBody ConfigRequest request)
throws IOException {
...
}
ConfigRequest class:
public class ConfigRequest {
private AbstractConfig config;
// Abbr. other fields, and all getters and setters
}
AbstractConfig class, which is included in ConfigRequest
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,
include=JsonTypeInfo.As.PROPERTY,
property="type",
visible=true)
#JsonSubTypes({#Type(value=ConcreteAConfig.class, name="concreteA")})
public abstract class AbstractConfig {
public AbstractConfig(){}
private String type;
// Abbr. other fields, and all getters and setters
}
Deserialized string:
{"type":"concreteA", ...}
Deserializing the json string fails
But when I try retrieving and deserializing(Phase 2), the deserialization fails:
16/03/24 17:17:20 ERROR (...Abbr...) org.codehaus.jackson.map.JsonMappingException: Can not construct instance of AbstractConfig, problem: abstract types can only be instantiated with additional type information
RowMapper, which raises the error:
public class BatchRowMapper implements RowMapper<Batch> {
private static ObjectMapper objectMapper = new ObjectMapper();
#Override
public Batch mapRow(ResultSet row, int rowNum) throws SQLException {
Batch batch = new Batch();
try {
// THIS METHOD RAISES THE ERROR
batch.setConfig(objectMapper.readValue(row.getString(CONFIG), AbstractConfig.class));
} catch (ClassCastException|IOException e) {
throw new SQLException(e.toString());
}
return batch;
}
}
I'd like to ser/de an abstract field with the same "type" field, and using only annotaions is my wish... Is it possible? If further information needed, I will be willing.
Thank you in advance.
I've found it's the problem what Jackson I used.
When I use jackson of codehause(older), deserialization with JsonTypeInfo doesn't work properly. Jackson of fasterxml works perfectly.
i have a rest controller in a spring boot mvc container
#RestController
public class LoginController {
#RequestMapping("rest/login")
public Response login() {
return Response.GRANTED;
}
public static enum Response {
GRANTED, DENIED;
}
}
I have to use double quotes for checking the return type after request a rest resource. how to avoid the double quotes?
$http.post("rest/login", $scope.data).success(function(data) {
if (data === "\"GRANTED\"") {
alert("GRANTED")
} else if (data === "DENIED") {
alert("DENIED")
};
#RestController
public class LoginController {
#RequestMapping("rest/login")
public String login() {
return Response.GRANTED.name();
}
public static enum Response {
GRANTED, DENIED;
}
}
bring the result I want but I want the type safe return type Response and not String.
Thanks for help.
A #RestController is like a #Controller annotated with #ResponseBody. That is, each handler is implicitly annotated with #ResponseBody. With any reference type other than String (and a few others), the default target content-type is JSON.
The 6 data types in JSON are Object, Array, Number, String, true, false, and null. How would you map an enum constant? The default that Jackson (which backs the default JSON HttpMessageConverter) serializes an enum constant to a JSON String. That's arguably the best matching JSON data type.
You could force it to write the value without quotes by providing your own JsonSerializer
#JsonSerialize(using = ResponseSerializer.class)
public static enum Response {
...
class ResponseSerializer extends JsonSerializer<Response> {
#Override
public void serialize(Response value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeRaw(value.name());
}
}
but I don't recommend it since you wouldn't be producing valid JSON.
You should really consider what others have suggested and use the various HTTP status codes.