2

I have a problem with Jackson in SpringBoot. My controller returns dates in format yyyy-MM-dd'T'HH:mm:ss'Z', but I need yyyy-MM-dd'T'HH:mm:ss.SSS'Z' (for mwl-calendar in angular which uses date-fns library).

Controller:

@GetMapping(value = "/slots", produces = MediaType.APPLICATION_JSON_VALUE)
public Set<SlotResponse> timeSlots() {
    return slotService.getSlots();
}

SlotResponse:

@Data
public class SlotResponse {
    private Instant start;
    private Instant end;
}

Additional dependency:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

I tried:

  1. to use @JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") annotation
  2. to use spring.jackson.date-format=yyyyMMddTHH:mm:ss.SSSZ configuration
  3. to create ObjectMapper manually:
@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
    return mapper;
}
  1. to enable options SerializationFeature.WRITE_DATES_WITH_ZONE_ID and SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE

But all of this didn't take effect. Everytime I see this result:

[{"start":"2023-05-11T16:00:00Z","end":"2023-05-11T17:00:00Z"}]

I use Java 17, SpringBoot 2.7.0, Jackson 2.13.3

What is wrong?

Virkom
  • 373
  • 4
  • 22
  • 1
    Your mwl-calendar is intolerant. It should just assume the fraction of second is zero when it is not in the string. The format is [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) standard. According to the standard, the fraction of second is optional. – Ole V.V. May 11 '23 at 08:27
  • @OleV.V., maybe. It uses date-fns library and it says "Incorrect date" if I use `yyyy-MM-ddTHH:mm:ssZ`. I didn't go deeper in this problem and tried to fix it on backend side. – Virkom May 11 '23 at 08:42

2 Answers2

3

You can create custom json serializer for Instant type:

    public static class InstantSerializer extends StdSerializer<Instant> {

        public InstantSerializer() {
            super(Instant.class);
        }

        @Override
        public void serialize(
            @Nullable final Instant value,
            @NonNull final JsonGenerator jsonGenerator,
            @NonNull final SerializerProvider serializerProvider
        ) throws IOException {
            if (value != null) {
                final DateTimeFormatter formatter = DateTimeFormatter
                    .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
                    .withZone(ZoneId.systemDefault());
                final String text = formatter.format(value);
                jsonGenerator.writeString(text);
            }
        }

    }

Then use it on the field that you want to follow the format:

    @Data
    public class SlotResponse {

        @JsonSerialize(using = InstantSerializer.class) // Add this
        
        private Instant start;
        @JsonSerialize(using = InstantSerializer.class) // Add this
        private Instant end;
    }

In case you want to make this apply for your whole system, you can add the serializer to jackson:

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        ...
        final SimpleModule module = new SimpleModule();
        module.addSerializer(new InstantSerializer());
        mapper.registerModule(module);
        ...
        return mapper;
    }

Ananta
  • 553
  • 4
  • 9
1

Sorry, cannot reproduce

With (only)

  • spring-boot-starter-web:2.7.0
  • lombok
  • (dev tools, spring-boot-starter-test)

, (shortened) "dto":

package com.example.soq76225352;

import java.time.Instant;
import lombok.Data;

@Data
public class SomeResponse {
  private Instant start = Instant.now();
}

and:

@RestController
class DemoController {

  @GetMapping(value = "/slots")
  public SomeResponse timeSlots() {
    return new SomeResponse();
  }
}

(no other settings, properties, only defaults..) , we get:

{"start":"2023-05-11T09:08:23.569050400Z"}

which looks very like the desired format. (??)


As soon I add:

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")

, I get:

2023-05-11 11:03:49.487  WARN 3732 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver :
   Resolved [org.springframework.http.converter.HttpMessageNotWritableException:
     Could not write JSON: Unsupported field: YearOfEra; nested exception is com.fasterxml.jackson.databind.JsonMappingException:
       Unsupported field: YearOfEra (through reference chain: 
         com.example.soq76225352.SomeResponse["start"])
   ]

(-> /error -> 404) !?


Going deeper

  • The format 'Z' refers to a "literal Z"
  • Whereas Z refers to the "zone offset" (in form +HHMM, see DateTimeFormatterBuilder + source documentation)
  • On the other hand: X is the "zone offset" (as described by Z) or (literal) 'Z' in case of "zero offset"/UTC! DateTimeFormatter :)

To make it work:

  • in all cases (that's strange for the literal case!), we have to:

    • or @JsonFormat(timezone = "..." //,...
    • or spring.jackson.time-zone

    ... (explicitly) set the "config time zone", see Set Jackson Timezone for Date deserialization

    • additionally we need to ensure (it is by default): spring.jackson.serialization.WRITE_DATES_WITH_CONTEXT_TIME_ZONE=true , see source code(your version)

Given (e.g.)

spring.jackson.time-zone=UTC

When/Then

  • When format is 'Z', then we get "...Z"
  • When format is Z, then we get "...+0000"
  • When format is X, then we get (also) "...Z"

Relevant, but old:

Relevant, but not deep/exact (milliseconds) enough:

xerx593
  • 12,237
  • 5
  • 33
  • 64