Description
I'm new to Java AND Jackson and I try to save a java.time.duration
to a JSON in a nice and readable hh:mm (hours:minutes) format for storing and retrieving.
In my project I use:
- Jackson
com.fasterxml.jackson.core:jackson-databind:2.14.1
. - Jackson
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1
for the support of the newer Java 8 time/date classes.
Minimum working example:
Consider following example class:
public class Book {
private Duration timeToComplete;
public Book(Duration durationToComplete) {
this.timeToComplete = durationToComplete;
}
// default constructor getter & setter
}
If I try to serialize a book instance into JSON like in the following code section
public class JavaToJson throws JsonProcessingException {
public static void main(String[] args) {
// create the instance of Book, duration 01h:11min
LocalTime startTime = LocalTime.of(13,30);
LocalTime endTime = LocalTime.of(14,41);
Book firstBook = new Book(Duration.between(startTime, endTime));
// create the mapper, add the java8 time support module and enable pretty parsing
ObjectMapper objectMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build()
.enable(SerializationFeature.INDENT_OUTPUT);
// serialize and print to console
System.out.println(objectMapper.writeValueAsString(firstBook));
}
}
it gives me the duration in seconds instead of 01:11
.
{
"timeToComplete" : 4740.000000000
}
How would I change the JSON output into a hh:mm format?
What I tried until now
I thought about adding a custom Serializer/Deserializer (potentially a DurationSerializer
?) during instantiation of the ObjectMapper but it seems I can't make the formatting work...
ObjectMapper objectMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
// add the custom serializer for the duration
.addModule(new SimpleModule().addSerializer(new DurationSerializer(){
@Override
protected DurationSerializer withFormat(Boolean useTimestamp, DateTimeFormatter dtf, JsonFormat.Shape shape) {
// here I try to change the formatting
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm");
return super.withFormat(useTimestamp, dtf, shape);
}
}))
.build()
.enable(SerializationFeature.INDENT_OUTPUT);
All it does is change it to this strange textual representation of the Duration:
{
"timeToComplete" : "PT1H11M"
}
So it seems I'm not completely off but the formatting is still not there. Maybe someone can help with the serializing/de-serializing?
Thanks a lot
CodePudding user response:
hh:mm
format is not supported by Jackson
since Java
does not recognise it by default. We need to customise serialisation/deserialisation mechanism and provide custom implementation.
Take a look at:
Using some examples from linked articles I have created custom serialiser and deserialiser. They do not handle all possible cases but should do the trick for your requirements:
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class DurationApp {
public static void main(String[] args) throws JsonProcessingException {
LocalTime startTime = LocalTime.of(13, 30);
LocalTime endTime = LocalTime.of(14, 41);
Book firstBook = new Book(Duration.between(startTime, endTime));
// create the mapper, add the java8 time support module and enable pretty parsing
ObjectMapper objectMapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.addModule(new SimpleModule()
.addSerializer(Duration.class, new ApacheDurationSerializer())
.addDeserializer(Duration.class, new ApacheDurationDeserializer()))
.build()
.enable(SerializationFeature.INDENT_OUTPUT);
String json = objectMapper.writeValueAsString(firstBook);
System.out.println(json);
Book deserialisedBook = objectMapper.readValue(json, Book.class);
System.out.println(deserialisedBook);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Book {
@JsonFormat(pattern = "HH:mm")
private Duration duration;
}
class ApacheDurationSerializer extends DurationSerializer {
private final String apachePattern;
public ApacheDurationSerializer() {
this(null);
}
ApacheDurationSerializer(String apachePattern) {
this.apachePattern = apachePattern;
}
@Override
public void serialize(Duration duration, JsonGenerator generator, SerializerProvider provider) throws IOException {
if (Objects.nonNull(apachePattern) && Objects.nonNull(duration)) {
String value = DurationFormatUtils.formatDuration(duration.toMillis(), apachePattern);
generator.writeString(value);
} else {
super.serialize(duration, generator, provider);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
JsonFormat.Value format = findFormatOverrides(prov, property, handledType());
if (format != null && format.hasPattern() && isApacheDurationPattern(format.getPattern())) {
return new ApacheDurationSerializer(format.getPattern());
}
return super.createContextual(prov, property);
}
private boolean isApacheDurationPattern(String pattern) {
try {
DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), pattern);
return true;
} catch (Exception e) {
return false;
}
}
}
class ApacheDurationDeserializer extends DurationDeserializer {
private final String apachePattern;
private final int numberOfColonsInPattern;
public ApacheDurationDeserializer() {
this(null);
}
ApacheDurationDeserializer(String apachePattern) {
this.apachePattern = apachePattern;
this.numberOfColonsInPattern = countColons(apachePattern);
}
@Override
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException {
if (Objects.nonNull(apachePattern)) {
String value = parser.getText();
if (this.numberOfColonsInPattern != countColons(value)) {
throw new JsonMappingException(parser, String.format("Pattern '%s' does not match value '%s'!", apachePattern, value));
}
if (numberOfColonsInPattern == 0) {
return Duration.ofSeconds(Long.parseLong(value.trim()));
}
String[] parts = value.trim().split(":");
return switch (parts.length) {
case 1 -> Duration.ofSeconds(Long.parseLong(value.trim()));
case 2 -> Duration.ofSeconds(TimeUnit.HOURS.toSeconds(Long.parseLong(parts[0]))
TimeUnit.MINUTES.toSeconds(Long.parseLong(parts[1])));
case 3 -> Duration.ofSeconds(TimeUnit.HOURS.toSeconds(Long.parseLong(parts[0]))
TimeUnit.MINUTES.toSeconds(Long.parseLong(parts[1]))
Long.parseLong(parts[2]));
default ->
throw new JsonMappingException(parser, String.format("Pattern '%s' does not match value '%s'!", apachePattern, value));
};
} else {
return super.deserialize(parser, context);
}
}
@Override
public Duration deserialize(JsonParser p, DeserializationContext ctxt, Duration intoValue) throws IOException {
return super.deserialize(p, ctxt, intoValue);
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
if (format != null && format.hasPattern() && isApacheDurationPattern(format.getPattern())) {
return new ApacheDurationDeserializer(format.getPattern());
}
return super.createContextual(ctxt, property);
}
private boolean isApacheDurationPattern(String pattern) {
try {
DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), pattern);
return true;
} catch (Exception e) {
return false;
}
}
private static int countColons(String apachePattern) {
return StringUtils.countMatches(apachePattern, ':');
}
}
Above code prints:
{
"duration" : "01:11"
}
Book(duration=PT1H11M)