Efficient way to have Jackson serialize Java 8 Instant as epoch milliseconds?
Asked Answered
P

6

21

Using Spring RestControllers with Jackson JSON parsing backend, with AngularJS on front end. I'm looking for an efficient way to have Jackson serialize an Instant as the epoch milliseconds for subsequent convenient usage with JavaScript code. (On the browser side I wish to feed the epoch ms through Angular's Date Filter: {{myInstantVal | date:'short' }} for my desired date format.)

On the Java side, the getter that Jackson would use is simply:

public Instant getMyInstantVal() { return myInstantVal; }

Serialization wouldn't work as-is, because the jackson-datatype-jsr310 doesn't return Epoch milliseconds by default for an Instant. I looked at adding @JsonFormat to the above getter to morph the Instant into something the front-end can use, but it suffers from two problems: (1) the pattern I can supply it is apparently limited to SimpleDateFormat which doesn't provide an "epoch milliseconds" option, and (2) when I tried to send the Instant as a formatted date to the browser instead, Jackson throws an exception because the @JsonFormat annotation requires a TimeZone attribute for Instants, something I don't wish to hardcode as it would vary from user to user.

My solution so far (and it's working fine) is to create a replacement getter using @JsonGetter, which causes Jackson to use this method instead to serialize myInstantVal:

@JsonGetter("myInstantVal")
public long getMyInstantValEpoch() {
    return myInstantVal.toEpochMilli();
}

Is this the proper way of doing this? Or is there a nice annotation I'm missing that I can put on getMyInstantVal() so I won't have to create these additional methods?

Protect answered 23/6, 2016 at 19:1 Comment(0)
G
42

You just need to read the README that you linked to. Emphasis mine:

Most JSR-310 types are serialized as numbers (integers or decimals as appropriate) if the SerializationFeature#WRITE_DATES_AS_TIMESTAMPS feature is enabled, and otherwise are serialized in standard ISO-8601 string representation.

[...]

Granularity of timestamps is controlled through the companion features SerializationFeature#WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS and DeserializationFeature#READ_DATE_TIMESTAMPS_AS_NANOSECONDS. For serialization, timestamps are written as fractional numbers (decimals), where the number is seconds and the decimal is fractional seconds, if WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS is enabled (it is by default), with resolution as fine as nanoseconds depending on the underlying JDK implementation. If WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS is disabled, timestamps are written as a whole number of milliseconds.

Guillermoguilloche answered 23/6, 2016 at 19:44 Comment(6)
I'm not sure how to set that value in the Spring XML config I'm using, I'm using Spring RestControllers and whatever default Jackson config it is using. Researching further...Protect
docs.spring.io/spring/docs/current/javadoc-api/org/…. But I would really avoid XML configuration. Java config is so much better.Guillermoguilloche
Thanks, I'll test this out. No, I'm a stubbornly proud member of the XML config club.Protect
How are the two other guys going? :-)Guillermoguilloche
Also for those who use spring-boot: you can set any jackson features from application.properties file using spring.jackson.* prefix. For example, WRITE_DATES_AS_TIMESTAMPS serialization feature can be enabled using spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=true line. See more: Appendix A. Common application properties.Dunedin
In my opinion, this is a bad solution. It impacts every other Instant field you use that ObjectMapper for and if you use a different ObjectMapper (like in a unit tests) it won't take effect.Uniliteral
P
8

This is what worked for me in Kotlin (should be the same for Java). This lets you serialize as an epoch millisecond without changing the ObjectMapper's configuration

data class MyPojo(
  @JsonFormat(without = [JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS])
  val timestamp: Instant
)
Prototherian answered 19/10, 2022 at 0:5 Comment(1)
The Java annotation syntax is slightly different; without = {JsonFormat.Feature...} with curly braces rather than square brackets.Technics
J
3

To apply for a single property, use this annotation:

@JsonFormat(shape = JsonFormat.Shape.NUMBER, without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
public Instant getMyInstantVal() { return myInstantVal; }
Johore answered 10/5, 2023 at 14:40 Comment(0)
P
1

Adding on to JB's answer, to override Spring MVC's default JSON parser to strip away the nanoseconds from Instant (and other Java 8 date objects that have them):

  1. In the mvc:annotation-driven element, specify that you will be overriding the default JSON message converter:

    <mvc:annotation-driven validator="beanValidator"> <mvc:message-converters register-defaults="true"> <beans:ref bean="jsonConverter"/> </mvc:message-converters> </mvc:annotation-driven>

(register-defaults above is true by default and most probably what you'll want to keep the other converters configured by Spring as-is).

  1. Override MappingJackson2HttpMessageConverter as follows:

    <beans:bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
    <beans:property name="objectMapper">
        <beans:bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
            <beans:property name="featuresToDisable">
                <beans:array>
                    <util:constant static-field="com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS"/>
                </beans:array>
            </beans:property>
        </beans:bean>
    </beans:property>
    

Step #1 is important as Spring MVC will otherwise ignore the configured MJ2HMC object in favor of its own default one.

partial H/T this SO post.

Protect answered 24/6, 2016 at 0:45 Comment(2)
Is there a non-XML solution of this?Aylsworth
@LeninRajRajasekaran Yup! https://mcmap.net/q/380383/-deserialize-millisecond-timestamp-to-java-time-instant/1207540Protect
T
0
  1. JsonFormat Annotation: The point above regarding the @JsonFormat annotation is valid. But if you need to handle both serialization and deserialization, you may want to ensure that the configuration accommodates both aspects. The provided code snippet addresses the serialization part, but you might also need to configure deserialization.
@JsonFormat(shape = JsonFormat.Shape.NUMBER, without = JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
public Instant getMyInstantVal() { return myInstantVal; }
  1. the proposed spring boot autoconfiguration doesn't work: https://github.com/FasterXML/jackson-modules-java8/issues/1

In order to deserialize the millis, you can use a custom config :

@Configuration
@RequiredArgsConstructor
public class JsonDateTimeSerdeConfig {
    private final RequestMappingHandlerAdapter handlerAdapter;

    @PostConstruct
    public void handleContextRefresh() {
        handlerAdapter
                .getMessageConverters()
                .forEach(c -> {
                    if (c instanceof MappingJackson2HttpMessageConverter) {
                        MappingJackson2HttpMessageConverter jsonMessageConverter = (MappingJackson2HttpMessageConverter) c;
                        ObjectMapper objectMapper = jsonMessageConverter.getObjectMapper();
                        objectMapper.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
                    }
                });
    }

For sure it will affect all the timestamps in the service, but for me it would be strange to have different serde strategies in the same service.

Teter answered 29/12, 2023 at 9:19 Comment(0)
B
-2

A simple way to return epoch millis in the JSON response for an Instant property can be following:

@JsonFormat(shape = JsonFormat.Shape.NUMBER, timezone = "UTC")
private Instant createdAt;

This will result in the following response:

{
  ...
  "createdAt": 1534923249,
  ...
}
Bennet answered 22/8, 2018 at 7:45 Comment(7)
This doesn't work for me. The @JsonFormat annotation seems to be ignored.Catmint
@ShadowMan Does @JsonFormat annotation work on other properties of the same class? If not then check if there isn't any custom serializers registered with your mapper.Bennet
fwiw both of these annotations on an Instant variable: @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT, timezone = "UTC") and @JsonFormat(shape = JsonFormat.Shape.NUMBER, timezone = "UTC") resulted in this unfortunate format for me: 1596745072.206033000Protractile
what's the point of timezone here? Epoch is always from Jan 1 1970 00:00 UTC.Uniliteral
@ShadowMan Did you manage to sort it out? I'm also still getting the default epoch+nanos object instead.Bootery
@Bootery yes I did: https://mcmap.net/q/117576/-java-8-localdate-jackson-format Basically, you just have to use custom de/serializers, which you can either setup in the ObjectMapper or as annotations on the model property.Catmint
Thanks @ShadowMan. I refused to accept implementing anything custom as an option for such an ordinary task. I've succeeded by adding: @JsonSerialize(using = InstantSerializer.class) along with shape.NUMBER. I did get the Decimal result featuring seconds.nanoseconds, and I couldn't find a viable option to make the formatter stick to the epochSeconds. Since this is a one-off for me - that's just good enough.Bootery

© 2022 - 2024 — McMap. All rights reserved.