9

I'm using Spring Boot with the following ObjectMapper:

@Bean
public ObjectMapper objectMapper()
{
    final ObjectMapper mapper = new ObjectMapper();

    mapper.enable(SerializationFeature.INDENT_OUTPUT);

    mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);  
    mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true)); // Makes no difference to output

    mapper.findAndRegisterModules();

    return mapper;
}

When OffsetDateTimes are serialized and returned in responses, they have a format like this:

"2020-02-28T12:28:29.01Z"
"2020-02-28T12:36:21.885Z"

I would have expected the timezone information at the end to look like this instead:

"2020-02-28T10:41:25.287+00:00"

Is there something I'm missing or doing wrong here, or anyway I can get the timezone information serialized as the +00:00 format instead of 885Z format?

Many thanks!

rmf
  • 625
  • 2
  • 9
  • 39

4 Answers4

11

The following steps resolve this (taken from https://stackoverflow.com/a/41893238/12177456), thanks also @Ralf Wagner and @deHaar:

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>2.6.5</version>
</dependency>
public class OffsetDateTimeSerializer extends JsonSerializer<OffsetDateTime>
{
    private static final DateTimeFormatter ISO_8601_FORMATTER = DateTimeFormatter
        .ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx")
        .withZone(ZoneId.of("UTC"));

    @Override
    public void serialize(OffsetDateTime value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException
    {
        if (value == null) {
            throw new IOException("OffsetDateTime argument is null.");
        }

        jsonGenerator.writeString(ISO_8601_FORMATTER.format(value));
    }
}
@Bean
public ObjectMapper objectMapper()
{

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    objectMapper.registerModule(new JavaTimeModule());
    SimpleModule simpleModule = new SimpleModule();

    simpleModule.addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer());
    objectMapper.registerModule(simpleModule);

    return objectMapper;
}
rmf
  • 625
  • 2
  • 9
  • 39
  • 1
    Looks cool, but doesn't help: `2019-11-30T10:30:00.123+03:00` is still converted to `2019-11-30T07:30:00.123Z` :( – Dmitriy Popov Jun 04 '20 at 12:02
  • 1
    `new ObjectMapper(),registerModule(new JavaTimeModule()).configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false).configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)` is what actually hepls with the problem from my previous comment. – Dmitriy Popov Jun 04 '20 at 14:22
  • I used exactly the same answer but for `ZonedDateTime` – stereo Aug 02 '22 at 13:41
7

The new Java 8 Time API provides a DateTimeFormatter where you can set the end of the format to one or more x or X. As per the api description:

Offset X and x: This formats the offset based on the number of pattern letters. One letter outputs just the hour, such as '+01', unless the minute is non-zero in which case the minute is also output, such as '+0130'. Two letters outputs the hour and minute, without a colon, such as '+0130'. Three letters outputs the hour and minute, with a colon, such as '+01:30'. Four letters outputs the hour and minute and optional second, without a colon, such as '+013015'. Five letters outputs the hour and minute and optional second, with a colon, such as '+01:30:15'. Six or more letters throws IllegalArgumentException. Pattern letter 'X' (upper case) will output 'Z' when the offset to be output would be zero, whereas pattern letter 'x' (lower case) will output '+00', '+0000', or '+00:00'.

So, in your case your formatting string should end in xxx for +1:30, e.g. "yyyy-MM-dd'T'HH:mm:ss.SSSxxx" with the SSS always giving the milliseconds.

To use this DateTimeFormatter with Jackson, you either need to define a custom serializer

public class DefaultZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {


  private static final DateTimeFormatter ISO_8601_FORMATTER = DateTimeFormatter
        .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
        .withZone(ZoneId.of("UTC"));

  @Override
  public void serialize(ZonedDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    if (value == null) {
        throw new IOException("ZonedDateTime argument is null.");
    }

    gen.writeString(ISO_8601_FORMATTER.format(value));
}

and annotate the respective fields in your beans with

@JsonSerialize(using = DefaultZonedDateTimeSerializer.class)
private ZonedDateTime someTimeProperty;

or you need to convert from DateTimeFormatter to DateFormat (older, but used by Jackson) as described here: Using DateTimeFormatter with ObjectMapper

Ralf Wagner
  • 1,467
  • 11
  • 19
6

There are several possibilities of using a pre-built formatting or providing a custom pattern to a DateTimeFormatter.

Have a look at these (very simple) examples:

public static void main(String[] arguments) {
    Instant now = Instant.now();
    
    ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(now, ZoneId.of("UTC"));
    OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(now, ZoneId.of("UTC"));
    
    System.out.println(zonedDateTime.toString());
    System.out.println(offsetDateTime.toString());
    System.out.println(zonedDateTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
    System.out.println(offsetDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
    System.out.println(zonedDateTime.format(
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"))
    );
    System.out.println(offsetDateTime.format(
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx")
        )
    );
}

I think what you are expecting is the last one, a pattern ending with xxx, which causes an offset to always be shown in the form HH:mm, the output of the code sample is:

2020-02-28T12:49:02.388Z[UTC]
2020-02-28T12:49:02.388Z
2020-02-28T12:49:02.388Z[UTC]
2020-02-28T12:49:02.388Z
2020-02-28T12:49:02+0000
2020-02-28T12:49:02+00:00
deHaar
  • 17,687
  • 10
  • 38
  • 51
0

If you are using JodaDateTime then try this:

    @Bean
    public ObjectMapper getObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JodaModule());
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return objectMapper;
    }
Prog_G
  • 1,539
  • 1
  • 8
  • 22