I have a rest controller with this method:
#RequestMapping(value = "", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<?> add(#Valid #RequestBody MyModel myModel, Errors errors) {
...
return new ResponseEntity<SomeObject>(someObject, HttpStatus.OK);
}
In MyModel has a field isMeetingOrSale that is enum (MeetingSaleFlag):
public enum MeetingSaleFlag {
MEETING("MEETING"),
SALE("SALE");
private final String name;
private MeetingSaleFlag(String s) { name = s; }
public boolean equalsName(String otherName) {
return (otherName == null) ? false : name.equals(otherName);
}
public String toString() { return this.name; }
}
and it can map a json that has a field "isMeetingOrSale" : "MEETING"
but the value in the json can be "isMeetingOrSale" : "" or completely missing, so in that case I want the field to be mapped to null. If I change the filed to be Optional<MeetingSaleFlag>
I got
Could not read JSON: Can not instantiate value of type [simple type,
class java.util.Optional<MeetingSaleFlag>] from String value
('MEETING'); no single-String constructor/factory method\\n at
[Source: java.io.PushbackInputStream#32b21158; line: 17, column: 18]
(through reference chain: MyModel[\"isMeetingOrSale\"]);
So the question is how can I map Optional enum from json?
Thanks to Sotirios Delimanolis's comment I was able to resolve the issue.
1) Add
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
as a dependency.
2) Reconfigure the Jackson mapper. Register:
#Bean
#Primary
public ObjectMapper jacksonObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
return mapper;
}
OR do this to register the jdk8 module
/**
* #return Jackson jdk8 module to be registered with every bean of type
* {#link ObjectMapper}
*/
#Bean
public Module jdk8JacksonModule() {
return new Jdk8Module();
}
Another way to customize Jackson is to add beans of type com.fasterxml.jackson.databind.Module to your context. They will be registered with every bean of type ObjectMapper, providing a global mechanism for contributing custom modules when you add new features to your application.
Doing this will only register the additional module and keep the built-in Jackson configuration provided by Spring Boot.
3) result
Now when the property is missing from the sent json, it's mapped to null
(This is not that great. I was expecting that it will give me an Optional and I will be able to use .isPresent()).
When it's an empty string ("isMeetingOrSale" : ""), Jackson returns an error:
Could not read JSON: Can not construct instance of
MyModel from String value '': value not
one of declared Enum instance names: [VAL1, VAL2]
which looks OK to me.
Useful links : Jackson jdk8 module, Spring MVC configure Jackson
This is an example from our codebase:
#NotNull // You probably don't want this
#JsonSerialize(using=CountrySerializer.class)
#JsonDeserialize(using=CountryDeserializer.class)
private CountryCode country;
where CountryCode is a complex enum (see nv-i18n) and these are the classes to (de)serialized from/to JSON:
public class CountrySerializer extends JsonSerializer<CountryCode> {
#Override
public void serialize(CountryCode value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeString(value.getAlpha3()); // Takes the Alpha3 code
}
public Class<CountryCode> handledType() { return CountryCode.class; }
}
and
public class CountryDeserializer extends JsonDeserializer<CountryCode> {
#Override
public CountryCode deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
// You can add here the check whether the field is empty/null
return CountryCode.getByCode(jp.getText());
}
}
You can easily replicate the same scenario using MeetingSaleFlag instead of CountryCode.
Related
I'm trying to map request parameters of a controller method into a POJO object, but only if any of its fields are present. However, I can't seem to find a way to achieve this. I have the following POJO:
public class TimeWindowModel {
#NotNull
public Date from;
#NotNull
public Date to;
}
If none of the fields are specified, I'd like to get an empty Optional, otherwise I'd get an Optional with a validated instance of the POJO. Spring supports mapping request parameter into POJO objects by leaving them unannotated in the handler:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId, #Valid TimeWindowModel timeWindow) {
// controller code
}
With this, Spring will map request parameters "from" and "to" to an instance of TimeWindowModel. However, I want to make this mapping optional. For POST requests you can use #RequestBody #Valid Optional<T>, which will give you an Optional<T> containing an instance of T, but only if a request body was provided, otherwise it will be empty. This makes #Valid work as expected.
When not annotated, Optional<T> doesn't appear to do anything. You always get an Optional<T> with an instance of the POJO. This is problematic when combined with #Valid because it will complain that "from" and "to" are not set.
The goal is to get either (a) an instance of the POJO where both "from" and "to" are not null or (b) nothing at all. If only one of them is specified, then #Valid should fail and report that the other is missing.
I came up with a solution with a custom HandlerMethodArgumentResolver, Jackson and Jackson Databind.
The annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestParamBind {
}
The resolver:
public class RequestParamBindResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper mapper;
public RequestParamBindResolver(ObjectMapper mapper) {
this.mapper = mapper.copy();
this.mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestParamBind.class) != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mav, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// take the first instance of each request parameter
Map<String, String> requestParameters = webRequest.getParameterMap()
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()[0]));
// perform the actual resolution
Object resolved = doResolveArgument(parameter, requestParameters);
// *sigh*
// see: https://stackoverflow.com/questions/18091936/spring-mvc-valid-validation-with-custom-handlermethodargumentresolver
if (parameter.hasParameterAnnotation(Valid.class)) {
String parameterName = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, resolved, parameterName);
// DataBinder constructor unwraps Optional, so the target could be null
if (binder.getTarget() != null) {
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.getErrorCount() > 0)
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
return resolved;
}
private Object doResolveArgument(MethodParameter parameter, Map<String, String> requestParameters) {
Class<?> clazz = parameter.getParameterType();
if (clazz != Optional.class)
return mapper.convertValue(requestParameters, clazz);
// special case for Optional<T>
Type type = parameter.getGenericParameterType();
Class<?> optionalType = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
Object obj = mapper.convertValue(requestParameters, optionalType);
// convert back to a map to find if any fields were set
// TODO: how can we tell null from not set?
if (mapper.convertValue(obj, new TypeReference<Map<String, String>>() {})
.values().stream().anyMatch(Objects::nonNull))
return Optional.of(obj);
return Optional.empty();
}
}
Then, we register it:
#Configuration
public class WebConfig implements WebMvcConfigurer {
//...
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestParamBindResolver(new ObjectMapper()));
}
}
Finally, we can use it like so:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId,
#RequestParamBind #Valid Optional<TimeWindowModel> timeWindow) {
// controller code
}
Which works exactly as you'd expect.
I'm sure it's possible to accomplish this by using Spring's own DataBind classes in the resolver. However, Jackson Databind seemed like the most straight-forward solution. That said, it's not able to distinguish between fields that are set to null and fields that just not set. This is not really an issue for my use-case, but it's something that should be noted.
To implement logic (a) both not null or (b) both are nulls you need to implement custom validation.
Examples are here:
https://blog.clairvoyantsoft.com/spring-boot-creating-a-custom-annotation-for-validation-edafbf9a97a4
https://www.baeldung.com/spring-mvc-custom-validator
Generally, you create a new annotation, it's just a stub, and then you create a validator which implements ConstraintValidator where you provide your logic and then you put your new annotation to your POJO.
I am new to Java SpringBoot and I am trying to inject a default value from application-dev.properties file.
Basically, the idea is to set default value to RequestObject's id property, default value being injected(/fetched) from application-dev.properties.
UseCase: In case, no data or blank data is being passed in request body of API, the code should set a default value (being injected/fetched from application-dev.properties).
Controller Code
#RequestMapping(value = PATH, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
private #ResponseBody ResponseEntity<ResponseBean<ResponseObject>> postController(
final #Valid #RequestBody RequestObject request) throws Exception {
return serviceClass.service(request);
}
I did the following in RequestObject Class code
public class RequestObject {
#Value("${package.default}")
private String default_id;
private String id = default_id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = !StringUtils.isBlank(id) ? id : default_id;
}
The above code is not working.
Please help or suggest a better way to achieve the requirement.
Note: If I hardcode the id value like id="abcd" while defining & in setter method, the code is working.
You can inject stuff(default values from application properties, other beans, etc.) only in spring managed components - those are classes annotated with #Component, #Service etc.
RequestObject, being a class you map request body to, is not, and should not be a component.
In order to inject default value from application.properties you need custom deserializer, which is a bean, and register this deserializer with ObjectMapper.
#Component
public class RequestObjectDeserializer extends StdDeserializer<RequestObject> {
#Value("${your.property.here}")
private String defaultValue;
public RequestObjectDeserializer() {
super(RequestObject.class);
}
#Override
public RequestObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
//deserialize
}
}
Then register it with already existing Objectmapper:
#Autowired
public void configObjectMapper(ObjectMapper objectMapper, RequestObjectDeserializer deserializer) {
SimpleModule module = new SimpleModule();
module.addDeserializer(RequestObject.class, deserializer);
objectMapper.registerModule(module);
}
You can add above in any #Configuration class.
I'd say keeping the default value as static field in RequestObject, and having the logic in setter, is better in this case.
why do you use #Value("${package.default}") in request body object? it should not work because they only mapped json into pojo object. you can use and initiate default_id using #Value("${package.default}") in your service class. after that you can set it your RequestObject .
I am trying to give an enum value as a header parameter to my rest endpoint in a spring boot #RestController. To that end I put the jackson libraries in my build.gradle file since the autogenerated enum used jackson annotations. I cannot change the enum code (it is autogenerated from a openapi specification). It looks like this:
public enum DocumentTypes {
APPLICATION_PDF("application/pdf"),
APPLICATION_RTF("application/rtf"),
APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT("application/vnd.oasis.opendocument.text"),
APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_WORDPROCESSINGML_DOCUMENT("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
APPLICATION_VND_MS_WORD("application/vnd.ms-word"),
TEXT_HTML("text/html"),
TEXT_PLAIN("text/plain");
private String value;
DocumentTypes(String value) {
this.value = value;
}
#Override
#JsonValue
public String toString() {
return String.valueOf(value);
}
#JsonCreator
public static DocumentTypes fromValue(String text) {
for (DocumentTypes b : DocumentTypes.values()) {
if (String.valueOf(b.value).equals(text)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + text + "'");
}
}
The restcontroller I am using to test looks like this:
#RestController
#RequestMapping("/test")
public class TestController {
#Autowired
private ObjectMapper objectMapper;
#RequestMapping(path = "", method = RequestMethod.GET)
public void test(#RequestHeader(value = "Accept", required = false) DocumentTypes targetFormat) throws IOException {
DocumentTypes value = objectMapper.readValue("\"application/pdf\"", DocumentTypes.class);
}
}
If I don't supply the Accept header and just let break inside the code I can see that the first line of the code works fine, the application/pdf String is transformed into value so the ObjectMapper did its job using the #JsonCreator method.
However if I pass Accept=application/pdf header along with the request I get an error:
Failed to convert value of type 'java.lang.String' to required type 'de.some.namespace.model.DocumentTypes';
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#org.springframework.web.bind.annotation.RequestHeader de.some.namespace.model.DocumentTypes] for value 'application/pdf';
nested exception is java.lang.IllegalArgumentException: No enum constant de.some.namespace.model.DocumentTypes.application/pdf"
This looks to me as if spring is not using the Jackson provided ObjectMapper, thus ignoring the #JsonCreator method and just trying to resolve the enum by default by looking if there is a key with that provided name.
This to me does not make sense, becuase I also only #Autowire the ObjectMapper,... isn't that the one that spring should also use, how can I force spring to use the correct one for parsing the arguments? I tried putting it into a #Configuration and making it a #Bean and #Primary with the same results.
I have a workaround by implementing a converter:
#Component
public class StringToDocumentTypesConverter implements Converter<String, DocumentTypes> {
#Autowired
private ObjectMapper mapper;
#Override
public DocumentTypes convert(String s) {
try {
return mapper.readValue(String.format("\"%s\"", s), DocumentTypes.class);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
But I don't understand why this would be necessary, normally spring automatically puts arguments through the ObjectMapper.
I think this is working as designed. Spring only uses the Jackson ObjectMapper for conversion of message bodies (using a registered HttpMessageConverter, specifically the MappingJackson2HttpMessageConverter).
This is documented at https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-typeconversion:
Some annotated controller method arguments that represent String-based request input (such as #RequestParam, #RequestHeader, #PathVariable, #MatrixVariable, and #CookieValue) can require type conversion if the argument is declared as something other than String.
For such cases, type conversion is automatically applied based on the configured converters
And https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-requestbody:
You can use the #RequestBody annotation to have the request body read and deserialized into an Object through an HttpMessageConverter
I made a Spring Boot 2 REST application. I'm consuming REST with Angular. I've a problem with enumeration.
A typical enum server side is:
public enum EngineType {
DIESEL, METHANE, ELECTRIC;
#Nullable
public static EngineType valueOfNullable(String value) {
try {
return valueOf(value);
} catch (Exception e) {
return null;
}
}
}
Some entities use these enum as fields and of course they can be null. Unfortunately, when the client do a POST of an entity sending "" (empty string) for the enumeration (because it can be null), I've an error server side:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `server.model.enums.EngineType` from String "": value not one of declared Enum instance names: [DIESEL, METHANE, ELECTRIC]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `server.model.enums.EngineType` from String "": value not one of declared Enum instance names: [DIESEL, METHANE, ELECTRIC]
at [Source: (PushbackInputStream); line: 1, column: 153] (through reference chain: server.model.tickets.Ticket["engineType2"])
I understand the sense of the message and I can solve the problem creating a custom deserializer as this:
#Component
public class EngineTypeDeserializer extends JsonDeserializer<EngineType> {
#Override
public EngineType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return EngineType.valueOfNullable(node.asText());
}
}
but I should put this annotation #JsonDeserialize(using = EngineTypeDeserializer.class) in all EngineType fields in my beans.
I was looking for a better way to solve this problem. Do you have some advice?
You can register your custom serializer programmatically.
In your #Configuration class:
#Bean
#Primary // Use this to shadow other objectmappers, if anny
public ObjectMapper objectMapper(){
ObjectMapper objMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(EngineType.class, new EngineTypeDeserializer());
objMapper.registerModule(module);
}
Our company is planning to switch our microservice technology to Spring Boot. As an initiative I did some advanced reading and noting down its potential impact and syntax equivalents. I also started porting the smallest service we had as a side project.
One issue that blocked my progress was trying to convert our Json request/response exchange to Spring Boot.
Here's an example of the code: (This is Nutz framework for those who don't recognize this)
#POST
#At // These two lines are equivalent to #PostMapping("/create")
#AdaptBy(type=JsonAdapter.class)
public Object create(#Param("param_1") String param1, #Param("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
On PostMan or any other REST client I simply pass POST:
{
"param_1" : "test",
"param_2" : 1
}
I got as far as doing this in Spring Boot:
#PostMapping("/create")
public Object create(#RequestParam("param_1") String param1, #RequestParam("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
I am not sure how to do something similar as JsonAdapter here. Spring doesn't recognize the data I passed.
I tried this but based on the examples it expects the Json paramters to be of an Entity's form.
#RequestMapping(path="/wallet", consumes="application/json", produces="application/json")
But I only got it to work if I do something like this:
public Object (#RequestBody MyModel1 model1) {}
My issue with this is that MyModel1 may not necessarily contain the fields/parameters that my json data has.
The very useful thing about Nutz is that if I removed JsonAdapter it behaves like a regular form request endpoint in spring.
I couldn't find an answer here in Stack or if possible I'm calling it differently than what existing spring devs call it.
Our bosses expect us (unrealistically) to implement these changes without forcing front-end developers to adjust to these changes. (Autonomy and all that jazz). If this is unavoidable what would be the sensible explanation for this?
In that case you can use Map class to read input json, like
#PostMapping("/create")
public Object create(#RequestBody Map<String, ?> input) {
sout(input.get("param1")) // cast to String, int, ..
}
I actually figured out a more straightforward solution.
Apparently this works:
#PostMapping("/endpoint")
public Object endpoint(#RequestBody MyWebRequestObject request) {
String value1 = request.getValue_1();
String value2 = request.getValue_2();
}
The json payload is this:
{
"value_1" : "hello",
"value_2" : "world"
}
This works if MyRequestObject is mapped like the json request object like so. Example:
public class MyWebRequestObject {
String value_1;
String value_2
}
Unmapped values are ignored. Spring is smart like that.
I know this is right back where I started but since we introduced a service layer for the rest control to interact with, it made sense to create our own request model object (DTOs) that is separate from the persistence model.
You can use #RequestBody Map as a parameter for #PostMapping, #PutMapping and #PatchMapping. For #GetMapping and #DeleteMapping, you can write a class which implements Converter to convert from json-formed request parameters to Map. And you would register that class as a bean with #Component annotation. Then you can bind your parameters to #RequestParameter Map.
Here is an example of Converter below.
#Component
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
private final ObjectMapper objectMapper;
#Autowired
public StringToMapConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public Map<String, Object> convert(String source) {
try {
return objectMapper.readValue(source, new TypeReference<Map<String, Object>>(){});
} catch (IOException e) {
return new HashMap<>();
}
}
}
If you want to exclude specific field of your MyModel1 class, use #JsonIgnore annotation onto the field like below.
class MyModel1 {
private field1;
#JsonIgnore field2;
}
Then, I guess you can just use what you have done.(I'm not sure.)
public Object (#RequestBody MyModel1 model1) {}
i think that you can use a strategy that involve dto
https://auth0.com/blog/automatically-mapping-dto-to-entity-on-spring-boot-apis/
you send a json to your rest api that is map like a dto object, after you can map like an entity or use it for your needs
try this:
Add new annotation JsonParam and implement HandlerMethodArgumentResolver of this, Parse json to map and get data in HandlerMethodArgumentResolver
{
"aaabbcc": "aaa"
}
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonParam {
String value();
}
#Component
public class JsonParamMethodResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonParam.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
RepeatedlyRequestWrapper nativeRequest = webRequest.getNativeRequest(RepeatedlyRequestWrapper.class);
if (nativeRequest == null) {
return null;
}
Gson gson = new Gson();
Map<String, Object> response = gson.fromJson(nativeRequest.getReader(), new TypeToken<Map<String, Object>>() {
}.getType());
if (response == null) {
return null;
}
JsonParam parameterAnnotation = parameter.getParameterAnnotation(JsonParam.class);
String value = parameterAnnotation.value();
Class<?> parameterType = parameter.getParameterType();
return response.get(value);
}
}
#Configuration
public class JsonParamConfig extends WebMvcConfigurerAdapter {
#Autowired
JsonParamMethodResolver jsonParamMethodResolver;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(jsonParamMethodResolver);
}
}
#PostMapping("/methodName")
public void methodName(#JsonParam("aaabbcc") String ddeeff) {
System.out.println(username);
}