I have mapped a custom deserializer to convert Strings on dd/MM/yyyy pattern to LocalDate so I can call my services with a more readable signature..
This is my dto class that is used as a Jersey #BeanParam to transport data between layers:
public class ProdutoFilterDto implements FilterDto {
private static final long serialVersionUID = -4998167328470565406L;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataInicial;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataFinal;
public LocalDate getDataInicial() {
return dataInicial;
}
public void setDataInicial(LocalDate dataInicial) {
this.dataInicial = dataInicial;
}
public LocalDate getDataFinal() {
return dataFinal;
}
public void setDataFinal(LocalDate dataFinal) {
this.dataFinal = dataFinal;
}
}
and this is my custom deserializer:
public class CustomLocalDateDeserializer extends JsonDeserializer<LocalDate> {
#Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
final String data = p.getValueAsString();
return (LocalDate) formatter.parse(data);
}
}
Its being used on this jersey service:
#Path("produto")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class ProdutoService {
...
#GET
#Path("query")
#Override
public Response query(
#QueryParam("offset") #DefaultValue(value = "0") Integer offSet,
#QueryParam("limit") #DefaultValue(value = "10") Integer limit,
#BeanParam ProdutoFilterDto filter) { ... }
...
}
I am calling like this:
${host goes here}/produto/query?dataInicial=11/09/1992
The problem is that the deserializer method is never called and the bean param variable remains null..
MessageBodyReaders aren't used for #QueryParam. You seem to be expecting the Jackson MessageBodyReader to handle this deserialization, but it doesn't work like that.
Instead you will want to use a ParamConverter, which will need to be registered through a ParamConverterProvider. For example:
#Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
#Override
public <T> ParamConverter<T> getConverter(
Class<T> rawType, Type genericType, Annotation[] antns) {
if (LocalDate.class == rawType) {
return new ParamConverter<T>() {
#Override
public T fromString(String string) {
try {
LocalDate localDate = LocalDate.parse(string, formatter);
return rawType.cast(localDate);
} catch (Exception ex) {
throw new BadRequestException(ex);
}
}
#Override
public String toString(T t) {
LocalDate localDate = (LocalDate) t;
return formatter.format(localDate);
}
};
}
return null;
}
}
Now LocalDate will work with #QueryParam and other #XxxParams also.
Some things to note:
If your goal is to parse both your #XxxParams and your JSON body into a bean this will not work. I'm not sure how that would work, but I'm sure it would involve a lot of hacking, and I wouldn't recommend it.
Your cast to (LocalDate) won't work. It's an illegal cast to java.time.format.Parsed. See correct way in code example.
Related to the above point. I was pulling out my hair for a good hour trying to figure out why I was getting a 404, using your parse code. With a 404, the last place I thought to look was in the ParamConverter. But it seems any uncaught exceptions that are thrown in the ParamConverter, will cause a 404. Doesn't make much sense right? The head pounding led me to this, which led me to this, which seems to be a poor specification
"if the field or property is annotated with
#MatrixParam, #QueryParam or #PathParam then an implementation MUST generate an instance of
NotFoundException (404 status) that wraps the thrown exception and no entity
"
Moral of the story: make sure to catch any possible exceptions in the ParamConverter!
See Also:
Good article on ParamConverters
Related
I am building my GraphQL schema for my project and one of my models has a DateTime format.
How do I write out date formats on my GraphQL schema?
I tried DateTime or Date but nothing shows up.
This is the model:
public Integer Id;
public String name;
public String description;
public LocalDate birthDate;
This is what's in my GraphQL schema:
type Pet {
id: ID!
name: String!
description: String
birthDate: DateTime
}
But it says:
Unknown type DateTime
Create a custom scalar for your types that is not recognized by your framework.
I am not sure which graphql-java based framework you are using. I assume you are using the official Spring for GraphQL from Spring team.
Create a custom scalar, eg my LocalDateTime scalar.
public class LocalDateTimeScalar implements Coercing<LocalDateTime, String> {
#Override
public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
if (dataFetcherResult instanceof LocalDateTime) {
return ((LocalDateTime) dataFetcherResult).format(DateTimeFormatter.ISO_DATE_TIME);
} else {
throw new CoercingSerializeException("Not a valid DateTime");
}
}
#Override
public LocalDateTime parseValue(Object input) throws CoercingParseValueException {
return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_DATE_TIME);
}
#Override
public LocalDateTime parseLiteral(Object input) throws CoercingParseLiteralException {
if (input instanceof StringValue) {
return LocalDateTime.parse(((StringValue) input).getValue(), DateTimeFormatter.ISO_DATE_TIME);
}
throw new CoercingParseLiteralException("Value is not a valid ISO date time");
}
}
Register it in your custom RuntimeWiring bean, check here.
public class Scalars {
public static GraphQLScalarType localDateTimeType() {
return GraphQLScalarType.newScalar()
.name("LocalDateTime")
.description("LocalDateTime type")
.coercing(new LocalDateTimeScalar())
.build();
}
}
#Component
#RequiredArgsConstructor
public class PostsRuntimeWiring implements RuntimeWiringConfigurer {
private final DataFetchers dataFetchers;
#Override
public void configure(RuntimeWiring.Builder builder) {
builder
//...
.scalar(Scalars.localDateTimeType())
//...
.build();
}
}
If you are using Scalars in other graphql-java based frameworks(GraphQL Java, GraphQL Java Kickstart, GraphQL Kotlin, GraphQL SPQR, Netflix DGS etc) and spring integrations, check my Spring GraphQL Sample. The back-end principle is similar, just some different config.
In my java dto i am converting json date ("dispOn": "11/28/2020",) to java.util.Date.since i am not sending any timezone jackson is taking UTC as default time zone but when desterilizing it is taking the offset of CST(My local time)as a result, date in my code(11/27/2020) is one day behind
import java.util.Date;
#JsonFormat(pattern = "MM/dd/yyyy")
private Date dispOn;
How can i get the same date sending in json into my code
As Andreas mentioned in his comment, it is sufficient to change from Date to LocalDate.
For additional info, I used LocalDateTime to show you how the format should be configured. I'm also using Lombok's annotations on class-level for logging and getter/setter generation.
Keep in mind that LocalDate and LocalDateTime are only available since Java 8.
Below a complete example of a DTO in which I parse a JSON
#Slf4j
#Getter
#Setter
#JsonIgnoreProperties(ignoreUnknown = true) // optional annotation
public class MyMessage implements Deserializer<MyMessage> {
#JsonProperty("EVENT_TIMESTAMP")
#JsonFormat(shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS")
private LocalDateTime eventTimestamp;
#JsonProperty("MESSAGE")
private String message;
#Override
public void configure(Map<String, ?> configs, boolean isKey) {
Deserializer.super.configure(configs, isKey);
}
#Override
public MyMessage deserialize(String s, byte[] bytes) {
ObjectMapper mapper = new ObjectMapper();
MyMessage event = null;
try {
event = mapper
.registerModule(new JavaTimeModule())
.readValue(bytes, MyMessage.class);
} catch (Exception e) {
log.error("Something went wrong during the deserialization of the MyMessage: {}", e.getMessage());
}
return event;
}
#Override
public MyMessage deserialize(String message, Headers headers, byte[] data) {
return Deserializer.super.deserialize(message, headers, data);
}
#Override
public void close() {
Deserializer.super.close();
}
And the JSON I'm sending:
{
"EVENT_TIMESTAMP": "2022-03-15T14:14:05.945",
"MESSAGE": "Test 123"
}
I have the following DTO and VO:
VO
public class ProjectVO {
private Date fechaInicio;
private Date fechaFin;
}
DTO
public class ProjectDTO {
private String fechaInicio;
private String fechaFin;
}
And the following converter to convert the strings to dates:
Converter<String, Date> dateConverter = new Converter<String, Date>()
{
public Date convert(MappingContext<String, Date> context)
{
Date date;
try {
date = new SimpleDateFormat("dd/MM/yyyy").parse(context.getSource());
} catch (ParseException e) {
throw new DateFormatException();
}
return date;
}
};
modelMapper.addConverter(dateConverter);
If I convert a single String to a date using modelmapper it'll work perfectly with this converter.
But now I need to convert my ProjectDTO object to a ProjectVO one and I am getting an error saying that it cannot convert a String to a Date. I suspect this is because the Date is inside the Project object. Am I right? How can I solve this?
Thanks.
Okay it works like a charm and automatically detects it if I use this:
modelMapper.createTypeMap(String.class, Date.class);
and then add the custom converter I created :)
I'm learning the basics of angular with a simple todo app. I have a simple spring boot backend which works fine. I'm currently struggling the best way to pass the date from a angular bootstrap datepicker to the backend. As the code is now the datatypes do not match.
Is the ideal way to convert it to seconds and convert the seconds back to a java date on the backend?
My ToDo entity:
#Id
#GeneratedValue
private long id;
private #NonNull
String taskName;
private Date dueDate;
private String extraNote;
private boolean taskCompleted;
Where I get the user input when he creates a new todo:
#Input() toDoData = { taskName: '', taskCompleted: false, extraNote: '', dueDate: Date};
addToDo() {
this.todoService.addToDo(this.toDoData).subscribe((result) => {
this.todoService.addToDo(this.toDoData);
});
}
Add todo part of my todoService:
addToDo(todo): Observable<any> {
console.log(todo);
return this.http.post<any>(this.API + 'todos', JSON.stringify(todo), this.httpOptions).pipe(
tap((todo) => console.log(`added todo w/ id=${todo.id}`)),
catchError(this.handleError<any>('addTodo'))
);
}
Thanks for any help!
EDIT (Added ToDoController):
#RestController
public class ToDoController {
private ToDoRepository repository;
public ToDoController(ToDoRepository repository) {
this.repository = repository;
}
#GetMapping("/todos")
List<ToDo> all() {
return repository.findAll();
}
#PostMapping("/todos")
ToDo newToDo(#RequestBody ToDo newToDo) {
return repository.save(newToDo);
}
#GetMapping("/todos/{id}")
ToDo one(#PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new ToDoNotFoundException(id));
}
#PutMapping("/todos/{id}")
ToDo replaceToDo(#RequestBody ToDo newToDo, #PathVariable Long id) {
return repository.findById(id)
.map(toDo -> {
toDo.setTaskName(newToDo.getTaskName());
toDo.setDueDate(newToDo.getDueDate());
toDo.setExtraNote(newToDo.getExtraNote());
toDo.setTaskCompleted(newToDo.getTaskCompleted());
return repository.save(toDo);
})
.orElseGet(() -> {
newToDo.setId(id);
return repository.save(newToDo);
});
}
#DeleteMapping("/todos/{id}")
void deleteToDo(#PathVariable Long id) {
repository.deleteById(id);
}
#GetMapping("/deleteall")
#CrossOrigin(origins = "http://localhost:4200")
public void deleteAll() {
repository.deleteAll();
}
#GetMapping("/init")
#CrossOrigin(origins = "http://localhots:4200")
public void createDefaults() {
Date date = new Date();
repository.save(new ToDo("PMB", date, false));
repository.save(new ToDo("GMDU", date, false));
repository.save(new ToDo("INMA", date, true));
repository.save(new ToDo("SLGP", date, false));
}
}
First of all. When using dates you should take into account summer/winter time issues and therefore I would suggest to use a LocalDate(Time) class.
That said:
I would create a
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper serializingObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// Mirror the default Spring Boot Jackson settings
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateSerializer());
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateDeserializer());
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
public static class LocalDateSerializer extends JsonSerializer<LocalDateTime> {
#Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
}
public static class LocalDateDeserializer extends JsonDeserializer<LocalDateTime> {
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
}
}
}
that converts all dates/times etc to milliseconds and from milliseconds back to localDate(Time). You can find plenty examples on the internet.
This acts as a filter for all dates/times that pass the endpoints in your application.
That way you do not need to concern yourself with any conversion issues anymore.
Then you will need to implement a dateToMillisecond and visa versa routine in Angular and use it where you need to convert te dates. You could intercept http traffic and do the same, but that might be a bit more complicated to implement.
Sample
function timeFromMillis(millis) {
if (_.isEmpty(millis)) {
return undefined;
}
const momentTimeStamp = moment.unix(millis / 1000);
if (!momentTimeStamp.isValid()) {
return undefined;
}
return momentTimeStamp.toDate();
}
function timeToMillis(obj) {
if (!_.isEmpty(obj)) {
if (obj instanceof moment) {
return obj.valueOf();
} else if (obj instanceof Date) {
return obj.getTime();
else if (angular.isString(obj)) {
return parseDateString(obj).getTime();
} else {
return angular.undefined;
}
}
}
function parseDateString(dateString) {
if (angular.isDefined(dateString) && dateString) {
return moment(dateString, 'YYYY-MM-DD').toDate();
}
}
you have 2 options using timeStamp as long pasing long from angular to beckend and from back to angular This is the way i prefer because timestamp is unique
Or you can use custom serializer deserializer
public class CustomDateTimeSerializer extends StdSerializer<DateTime> {
private static DateTimeFormatter formatter =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
public CustomDateTimeSerializer() {
this(null);
}
protected CustomDateTimeSerializer(Class<DateTime> t) {
super(t);
}
#Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
gen.writeString(formatter.print(value));
}
public class CustomDateTimeDeserializer extends JsonDeserializer<DateTime> {
#Override
public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
DateTime dateTime = formatter.parseDateTime(jsonParser.getText());
return dateTime;
}
}
ps: i have used yoda time in serializer and deserializer
I ran into this and it turned out to be timezone issue. From frontend it was sending json with "2019-04-01" but backend was converting it to "2019-03-31"
A lot of the code is not using java.time so I found adding below to application.properties file to be best solution. I also added global date format, but change timezone to what you need.
spring.jackson.date-format=yyyy-MM-dd
spring.jackson.time-zone=America/Chicago
I am trying to setup my spring boot configuration to use my custom serializer and deserializer.
Deserializer code
public class CustomZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
#Override
public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
ObjectCodec oc = jsonParser.getCodec();
TextNode node = (TextNode) oc.readTree(jsonParser);
String dateString = node.textValue();
return ZonedDateTime.parse(dateString, CustomZonedDateTimeSerializer.formatter);
}
#Override
public Class<?> handledType() {
return ZonedDateTime.class;
}
}
Serializer code
public class CustomZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
#Override
public Class<ZonedDateTime> handledType() {
return ZonedDateTime.class;
}
public final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
#Override
public void serialize(ZonedDateTime date, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
final String dateString = date.format(formatter);
generator.writeString(dateString);
}
}
This my configuration:
#Configuration
public class JacksonConfiguration {
#Autowired
Jackson2ObjectMapperBuilder jacksonBuilder;
#PostConstruct
public void configureJackson() {
final CustomZonedDateTimeSerializer serializer = new CustomZonedDateTimeSerializer();
jacksonBuilder.serializers(serializer);
final CustomZonedDateTimeDeserializer deserializer = new CustomZonedDateTimeDeserializer();
jacksonBuilder.deserializers(deserializer);
}
}
I am creating a REST API so I have this in my controller:
#RequestMapping(value = "date", method = { RequestMethod.GET })
#ApiOperation(value = "", notes = "")
public ResponseWrapper<String> testDATE(
#RequestParam #ApiParam(value = "", required = true) ZonedDateTime date) {
System.out.println(date.toString());
return new ResponseWrapper<String>(date.toString());
}
So far I have tried:
creating a custom module and adding it as a #Bean in the configuration, adding the serializer and deserializer to the jackonBuilder object in configuration and something like the code in this example
Am I missing something or doing something wrong?
Each time I try I get the following error:
"Failed to convert value of type 'java.lang.String' to required type
'java.time.ZonedDateTime'; nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to
convert from type [java.lang.String] to type
[#org.springframework.web.bind.annotation.RequestParam
#io.swagger.annotations.ApiParam java.time.ZonedDateTime] for value
'2018-07-10T00:00:00+0000'; nested exception is
java.lang.IllegalArgumentException: Parse attempt failed for value
[2018-07-10T00:00:00+0000]"
I am sending the following value each time: 2018-07-10T00:00:00+0000 and the value doesn't even reach the deserializer where I have my breakpoint.
Add this bean in your configuration class:
#Bean
public Jackson2ObjectMapperBuilder configureObjectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
final CustomZonedDateTimeSerializer serializer = new CustomZonedDateTimeSerializer();
builder.serializers(serializer);
final CustomZonedDateTimeDeserializer deserializer = new CustomZonedDateTimeDeserializer();
builder.deserializers(deserializer);
return builder;
}
Seems like I could not send ZonedDateTime in GET requests. Once I changed to POST, Spring was able to parse the date automatically.
I only added spring.jackson.deserialization.adjust_dates_to_context_time_zone=true so it takes the timezone into consideration.
If someone will want to send it in GET he will have to send a string and parse that instead of defining ZonedDateTime as a parameter.