What is the correct way to register a custom deserializer for a path variable in Spring web reactive?
Example:
#GetMapping("test/{customType}")
public String test(#PathVariable CustomType customType) { ...
I tried ObjectMapperBuilder, ObjectMapper and directly via #JsonDeserialize(using = CustomTypeMapper.class) but it wont register:
Response status 500 with reason "Conversion not supported."; nested exception is org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type '...CustomType'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type '...CustomType': no matching editors or conversion strategy found
For path variables deserialization you don't need to involve jackson, you can use org.springframework.core.convert.converter.Converter
For example:
#Component
public class StringToLocalDateTimeConverter
implements Converter<String, LocalDateTime> {
#Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(
source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}
#GetMapping("/findbydate/{date}")
public GenericEntity findByDate(#PathVariable("date") LocalDateTime date) {
return ...;
}
Here's an article with examples
I ended up using #ModelValue because it semantically is not a deserialization, just parsing a key.
#RestController
public class FooController {
#GetMapping("test/{customType}")
public String test(#ModelAttribute CustomType customType) { ... }
}
#ControllerAdvice
public class GlobalControllerAdvice {
#ModelAttribute("customType")
public CustomType getCustomType(#PathVariable String customType) {
CustomeType result = // map value to object
return result;
}
}
Related
I am calling a SOAP WS using Spring's WebServiceGatewaySupport and using reflection to set the request values. Spring version is 4.2.4.RELEASE and Spring WS version is 3.0.7.RELEASE.
Some of the expected attributes are of type XmlGregorianCalendar, so I created a custom XmlGregorianCalendarPropertyEditor for String<->XmlGregorianCalendar casting :
#Slf4j
public class XmlGregorianCalendarPropertyEditor extends PropertyEditorSupport {
private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
#Override
public String getAsText() {
XMLGregorianCalendar xmlGregorianCalendar = (XMLGregorianCalendar) getValue();
if (xmlGregorianCalendar != null) {
df.setTimeZone(xmlGregorianCalendar.toGregorianCalendar().getTimeZone());
return df.format(xmlGregorianCalendar.toGregorianCalendar().getTime());
} else {
return "";
}
}
#Override
public void setAsText(String text) {
if (StringUtils.isBlank(text)) {
setValue(null);
} else {
GregorianCalendar calendar = new GregorianCalendar();
try {
calendar.setTime(df.parse(text));
XMLGregorianCalendar xmlGregorianCalendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(calendar);
setValue(xmlGregorianCalendar);
} catch (ParseException e) {
log.error("Error parsing date {}", text);
setValue(null);
} catch (DatatypeConfigurationException e) {
log.error("Error building XmlGregorianCalendar!");
setValue(null);
}
}
}
This code could probably be better, I haven't been able to test it yet.
I have tried different ways of registering my PropertyEditor within my #Configuration class, but none have worked. Here's how it is right now :
#Bean
public CustomEditorConfigurer customEditorConfigurer() {
CustomEditorConfigurer customEditorConfigurer = new CustomEditorConfigurer();
customEditorConfigurer.setCustomEditors(Collections.singletonMap(XMLGregorianCalendar.class, XmlGregorianCalendarPropertyEditor.class));
return customEditorConfigurer;
}
Reflection-wise, I have a BeanWrapperImpl on the Request class and I simply use its method setPropertyValue(attributeName, value).
Here's the exception message I get :
Failed to convert property value of type [java.lang.String] to required type [javax.xml.datatype.XMLGregorianCalendar] for property 'dateCreationDocument'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [javax.xml.datatype.XMLGregorianCalendar] for property 'dateCreationDocument': no matching editors or conversion strategy found
I also tried creating two Converters and added this to my #Configuration class:
#Bean
public ConversionServiceFactoryBean conversionServiceFactoryBean() {
ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean();
Set<Converter> converters = new HashSet<>();
StringToXmlGregorianCalendar stringToXmlGregorianCalendar = new StringToXmlGregorianCalendar();
XmlGregorianCalendarToString xmlGregorianCalendarToString = new XmlGregorianCalendarToString();
converters.add(stringToXmlGregorianCalendar);
converters.add(xmlGregorianCalendarToString);
conversionServiceFactoryBean.setConverters(converters);
return conversionServiceFactoryBean;
}
But that leads to the same exception. I guess I could use the converters programatically (using #Autowired) but then that would mean more code and unnecessary type checking.
Thanks in advance.
I solved my issue by using this line everytime I used a BeanWrapperImpl:
BeanWrapperImpl beanWrapper = new BeanWrapperImpl(request);
beanWrapper.registerCustomEditor(XMLGregorianCalendar.class, new XmlGregorianCalendarPropertyEditor());
Not the most clean way to do things but it works.
Also, be careful with the CustomEditorConfigurer, I didn't make the bean static and it broke the annotations in my #Configuration class (so I couldn't use #Value for instance). Here's the warning that Spring gave me:
#Bean method ApplicationConfig.customEditorConfigurer is non-static and returns an object assignable to Spring's BeanFactoryPostProcessor interface. This will result in a failure to process annotations such as #Autowired, #Resource and #PostConstruct within the method's declaring #Configuration class. Add the 'static' modifier to this method to avoid these container lifecycle issues; see #Bean javadoc for complete details.
I have a Controller which accepts a POJO (MySearch) representing optional request parameters. Requests succeed when the id and/or name parameter(s) are included.
However, if I include the dateTime parameter like so:
GET /find?dateTime=2019-03-15T22:17:42Z&id=1432&name=Bob
I get an error:
Failed to convert from type [java.lang.String] to type [java.time.ZonedDateTime]
for value '2019-03-15T22:17:42Z'; nested exception is java.lang.IllegalArgumentException:
Parse attempt failed for value [2019-03-15T22:17:42Z]
I also tried annotating the dateTime field with #DateTimeFormat(iso = ISO.DATE_TIME), but that didn't help.
Serialization of ZonedDateTime fields in other tests successfully format the date/time similar to yyyy-MM-ddTHH:mm:ssZ in the output.
It is only the deserialization of the ZonedDateTime field in the POJO that I'm having difficulty with.
Controller
#RestController
public class MyController {
#GetMapping("/find")
public MyResponse find(MySearch search) {
// Do stuff ...
}
}
The POJO
public class MySearch {
private Integer id;
private String name;
private ZonedDateTime dateTime;
}
Configuration
I configured the ObjectMapper using these two beans:
#Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return Jackson2ObjectMapperBuilder.json()
.modulesToInstall(new Jdk8Module(), new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
#Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.build();
}
What am I missing, or doing wrong?
Thanks!
If you want to deserialize this object you need to register it as a new converter. In order to do so you can create a class similar to this one:
#Configuration
public class ZoneDateTimeWebMvcConfigurator implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToZoneDateTime());
}
static class StringToZoneDateTime implements Converter<String, ZonedDateTime> {
#Override
public ZonedDateTime convert(String value) {
return ZonedDateTime.parse(value);
}
}
}
I want to integrate vavr validation library in my command dto's in a way that when command dto is deserialized from request, return type of the static factory will be Try but jackson is throwing following error :
Type definition error: [simple type, class com.foo.command.FooCommand]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.foo.command.FooCommand (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
Here is FooCommand
#AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class FooCommand {
private String foo;
private String bar;
#JsonCreator
public static Try<FooCommand> of(
#JsonProperty("foo") String foo,
#JsonProperty("bar") String bar
) {
return Try.of(() -> {
//Validate values
});
}
}
I am using spring 5 and it's annotated to deserialize request body automatically into controller parameter.
Is something like this possible ? Thanks in advance.
I had a similar problem that I fixed by using Converters: Using Jackson, how can I deserialize values using static factory methods that return wrappers with a generic type?
I haven't yet found how to apply the converters automatically, so you have to annotate every occurrence of the wrapped type in your requests.
public class Request {
#JsonDeserialize(converter = FooCommandConverter.class)
Try<FooCommand> command;
}
You can write a Converter like so:
public class FooCommandConverter
extends StdConverter<FooCommandConverter.DTO, Try<FooCommand>> {
#Override
public Try<FooCommand> convert(FooCommandConverter.DTO dto) {
return FooCommand.of(
dto.foo,
dto.bar
);
}
public static class DTO {
public String foo;
public String bar;
}
}
I have an item I'd like to store in Dynamo:
public class Statement {
#DynamoDBTypeConverted(converter = ListLineItemConverter.class)
private List<LineItem> items;
}
and the definition of LineItem is the following:
public class LineItem {
private ZonedDateTime dateStart;
private ZonedDateTime dateEnd;
private long balance;
#DynamoDBTypeConverted(converter = ZonedDateTimeConverter.class)
public getDateStart() {...}
#DynamoDBTypeConverted(converter = ZonedDateTimeConverter.class)
public getDateEnd() {...}
}
I've been using a known working converter for ZonedDateTime which is the following:
public class ZonedDateTimeConverter implements DynamoDBTypeConverter<String, ZonedDateTime> {
public ZonedDateTimeConverter(){}
#Override
public String convert(final ZonedDateTime time) {
return time.toString();
}
#Override
public ZonedDateTime unconvert(final String stringValue) {
return ZonedDateTime.parse(stringValue);
}
}
And the converter works perfectly when it's annotated on a base class. But I have a custom type nested in a list of items and I can't seem to figure out how to get DynamoDB to correctly convert / unconvert a nested ZonedDateTime.
I even wrote a custom converter for List of LineItem without luck:
public class ListLineItemConverter implements DynamoDBTypeConverter<String, List<LineItem>> {
private ObjectMapper objectMapper;
public ListLineItemConverter() {
objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// THIS LINE OF CODE FIXED THE ISSUE FOR ME
objectMapper.findAndRegisterModules();
// THIS LINE OF CODE FIXED THE ISSUE FOR ME
}
#Override
public String convert(List<LineItem> object) {
try {
String result = objectMapper.writeValueAsString(object);
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException("bad json marshalling");
}
}
#Override
public List<LineItem> unconvert(String object) {
try {
return objectMapper.readValue(object, new TypeReference<List<LineItem>>() {});
} catch (IOException e) {
throw new RuntimeException("bad json unmarshalling");
}
}
}
There's no combination of annotations that I can seem to use to get this to work. I always get:
com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of java.time.ZonedDateTime: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
EDIT: If I comment out the instances of ZonedDateTime from LineItem then the code works totally fine. So DynamoDB is having trouble reading the #DynamoDBTypeConverted annotation when it's buried 3 levels deep:
Statement.items.get[0].dateStart // annotations aren't working at this level
Statement.items.get[0].dateEnd // annotations aren't working at this level
Looks like there was an issue when using the Jackson parser on nested instances of ZonedDateTime and it not automatically picking it up.
I've switched to the newer version of Jackson and even included the JSR310 module to ensure that compatibility with marshaling / unmarshaling the newer java 8 time constructs was supported but alas.
Weirdly one line of code fixed it for me:
objectMapper.findAndRegisterModules();
taken from: https://stackoverflow.com/a/37499348/584947
My problem is with having Spring bind the data I get from a form to a JPA entity. The wierd part is, it works just fine if I do not look at the BindingResults. The BindingResults says there were binding errors when an empty string is passed in for the field graduation, but I know it does bind them correctly because when I don't check Hibernate updates the database perfectly. Is there a way to not have to write logic to circumnavigate the wrongly fired binding errors?
#Entity
#Table(name="child")
public class Child {
#Id
#Column(name="id")
private Integer childId;
#ManyToOne(fetch=FetchType.EAGER )
#JoinColumn(name="house", referencedColumnName="house")
private House house;
#NotNull()
#Past()
#Column(name="birthday")
private Date birthday;
#Column(name="graduation_date")
private Date graduationDay;
}
I have tried the following lines in a property editor to no avail
SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy");
registry.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
Here is the method signature for the controller method Handling the request
#Controller
#SessionAttributes(value="child")
#RequestMapping(value="child")
public class ChildModController {
#RequestMapping(value="save-child.do", params="update", method = RequestMethod.POST)
public #ResponseBody Map<String,?> updateChild(
HttpServletRequest request,
#Valid #ModelAttribute(value="child")Child child,
BindingResult results)
}
This is what I get from the BindingResult class as a message
09:01:36.006 [http-thread-pool-28081(5)] INFO simple - Found fieldError: graduationDay,
Failed to convert property value of type java.lang.String to required type java.util.Date for property graduationDay;
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type java.lang.String to type #javax.persistence.Column java.util.Date for value ';
nested exception is java.lang.IllegalArgumentException
Spring automatically binds simple object types like String and Number, but for complex objects like java.util.Date or your own defined types, you will need to use what is called a PropertyEditors or Converters, both could solve your problem.
Spring already has a predefiend PropertyEditors and Converters like #NumberFormat and #DateTimeFormat
You can use them directly on your fields like this
public class Child {
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date birthday;
#DateTimeFormat(iso=ISO.DATE)
private Date graduationDay;
#NumberFormat(style = Style.CURRENCY)
private Integer myNumber1;
#NumberFormat(pattern = "###,###")
private Double myNumber2;
}
Spring also allows you to define your own type converters which you must use it combined with Spring ConversionService
For example if you have a Color class like this
public class Color {
private String colorString;
public Color(String color){
this.colorString = color;
}
}
You would define the color converter for example like this
public class StringToColor implements Converter<String, Color> {
public Color convert(String source) {
if(source.equal("red") {
return new Color("red");
}
if(source.equal("green") {
return new Color("green");
}
if(source.equal("blue") {
return new Color("blue");
}
// etc
return null;
}
}
To check more about converters check this, also check this to know the difference between Converters and PropertyEditors