I have recently updated to latest version of spring data elasticsearch
. And when I am trying to save or read data I am always getting the following error,
Unable to convert value '2022-01-30T20:44:43.786438' of property 'createdTime' Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds
Previously in old version I was using the following,
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
LocalDateTime createdTime;
So I tried the following now. But again it is throwing the same above error when saving data to elasticsearch. Reading data is fine with this way.
@Field(type = FieldType.Date, format = {}, pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSX")
LocalDateTime createdTime;
How can I resolve this issue? Why it is working fine when reading data but not when saving? And also I have old data which is saved in this date-time format. So I need a solution for this issue to overcome this error and also make this compatible with old data I have. Thanks in advance.
LocalDateTime
does not have a time zone. But you want to convert it with a pattern that has a time zone (X
). Therefore you get the error that the temporal object does not contain the offset (timezone) information.
You should change your definition to:
@Field(type = FieldType.Date, pattern = "uuuu-MM-dd'T'HH:mm:ss.SSS", format = {})
or use a ZoneDateTime
if you need timezones.
The reason why this worked in previous versions is that we could use some class from the Elasticsearch core libraries which did much custom handling and fallbacks. We cannot use these anymore now, so we use the standard DateTimeFormatter
from the JDK with the defined patterns, and that's sometimes less flexible.
Edit 31.01.2022:
As stated in the comment there are not only strings coming back from Elasticsearch that contain no zone information, there are as well cases where there is a zone information.
So you would need a LocalDateTime
for the cases when you need to pares a value that has no zone info, and a ZonedDateTime
when there is zone information available. There is no out of the box solution for this using only the @Field
annotation parameters, but as you wrote you are using the latest version of Spring Data Elasticsearch - that should be 4.3. And since this version you can define custom converters for single properties (not only the usual once for whole classes).
Let me show how to do this:
I change the class of the createdTime
property to ZonedDateTime
, when reading in data that does not have a zone info, I will set it to UTC. It would also be possible to keep LocalDateTime
and strip the timezone from an eventually read value.
Important is the additional annotation:
@Field(type = FieldType.Date, pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSX||uuuu-MM-dd'T'HH:mm:ss.SSS", format = {})
@ValueConverter(CustomZonedDateTimeConverter.class)
private ZonedDateTime someDate;
The reason to keep the @Field
annotation is to have the correct mapping written when the index is created by the application.
The @ValueConverter
annotation causes a converter of the given class to be created (must have a no-arg constructor) and attached to this property. It will not be used for any other ZonedDateTime
properties in the application, that makes it different form standard Spring Data converters. The implementation is as follows (Java 17 syntax in the instanceof
):
class CustomZonedDateTimeConverter implements PropertyValueConverter {
private final DateTimeFormatter formatterWithZone = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSX");
private final DateTimeFormatter formatterWithoutZone = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS");
@Override
public Object write(Object value) {
if (value instanceof ZonedDateTime zonedDateTime) {
return formatterWithZone.format(zonedDateTime);
} else {
return value;
}
}
@Override
public Object read(Object value) {
if (value instanceof String s) {
try {
return formatterWithZone.parse(s, ZonedDateTime::from);
} catch (Exception e) {
return formatterWithoutZone.parse(s, LocalDateTime::from).atZone(ZoneId.of("UTC"));
}
} else {
return value;
}
}
}
The writing part is simple, just write the zoned date time. On reading, I first try to parse a zoned date time. If that fails I try the parsing without any zone. On success I set the zone to UTC and return the value.
You could extend that to even more different formats if you need to.