Jackson Serialize Instant to Nanosecond Issue
Asked Answered
G

2

8

Jackson serialises java.time.Instant with WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS enabled by default.

It produces JSON like this

{ "timestamp":1421261297.356000000 }

I wonder if there's a way to get rid of the zeros at the end. I want something like:

{ "timestamp":1421261297.356 }

I tried:

mapper.configure( SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false );
mapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true );

But this configuration changes it to millisecond representation 1421261297356. I want the seconds part and the fractional milliseconds part.

Greaser answered 28/5, 2019 at 15:9 Comment(2)
Write your own custom (de-)serializer?Liborio
Whats wrong in having (propert) timestamp in miliseconds?Discourage
A
8

When we are working with Java 8 Time package and Jackson good idea is to use jackson-modules-java8 project which serves many serialisers and deserialisers ready to use. To enable it we need to register JavaTimeModule module. To serialise Instant InstantSerializer is used. When we check how it is implemented we will find out that behind scene DecimalUtils.toDecimal method is used. It looks like there is always zeros added at the end of nanoseconds value.

We can write our InstantSerializer which serialises it in desired way. Because classes in this project are not ready to easily extend we need to implement many unwanted methods and constructors. Also we need create in our project com.fasterxml.jackson.datatype.jsr310.ser package and create our implementation there. See below example:

package com.fasterxml.jackson.datatype.jsr310.ser;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;

public class ShortInstantSerializer extends InstantSerializerBase<Instant> {

    private ToLongFunction<Instant> getEpochSeconds = Instant::getEpochSecond;
    private ToIntFunction<Instant> getNanoseconds = i -> i.getNano() / 1_000_000;

    public ShortInstantSerializer() {
        super(Instant.class, Instant::toEpochMilli, Instant::getEpochSecond, Instant::getNano, null);
    }

    protected ShortInstantSerializer(ShortInstantSerializer base, Boolean useTimestamp, Boolean useNanoseconds, DateTimeFormatter formatter) {
        super(base, useTimestamp, useNanoseconds, formatter);
    }

    @Override
    protected JSR310FormattedSerializerBase<?> withFormat(Boolean useTimestamp, DateTimeFormatter formatter, JsonFormat.Shape shape) {
        return new ShortInstantSerializer(this, useTimestamp, null, formatter);
    }

    @Override
    public void serialize(Instant value, JsonGenerator generator, SerializerProvider provider) throws IOException {
        if (useTimestamp(provider)) {
            if (useNanoseconds(provider)) {
                generator.writeNumber(new BigDecimal(toShortVersion(value)));
                return;
            }
        }

        super.serialize(value, generator, provider);
    }

    private String toShortVersion(final Instant value) {
        return getEpochSeconds.applyAsLong(value) + "." + padWithZeros(getNanoseconds.applyAsInt(value));
    }

    private String padWithZeros(final int value) {
        return String.format("%1$3s", String.valueOf(value)).replace(' ', '0');
    }
}

And example how to use it:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.ShortInstantSerializer;

import java.time.Instant;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(Instant.class, new ShortInstantSerializer());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(javaTimeModule);
        mapper.disable(SerializationFeature.INDENT_OUTPUT);

        System.out.println(mapper.writeValueAsString(new Element()));
    }
}

class Element {

    private Instant timestamp = Instant.now();

    public Instant getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(Instant timestamp) {
        this.timestamp = timestamp;
    }
}

Above code prints:

{"timestamp":1559074287.223}

If you want to rid of all zeros in all cases write your own getNanoseconds function declared in ShortInstantSerializer class.

Absenteeism answered 28/5, 2019 at 20:12 Comment(4)
I can't override withFormat method because it says JSR310FormattedSerializerBase class is not public and can't be used outside package.Stomato
@oxyt, notice that ShortInstantSerializer class is created in com.fasterxml.jackson.datatype.jsr310.ser package. In your source folder create that package and move this class there. It should work after that.Resurrect
String concatenated = getEpochSeconds.applyAsLong(value) + "." + getNanoseconds.applyAsInt(value); doesn't return the right string if getNanoseconds.applyAsInt(value) is less than a 3-digit number. You have to pad it with 0's on the left. So if you have 1581385552 and 2 you get 1581385552.2 instead of 1581385552.002Factitive
@PawelZieminski, thank you for pointing me this out. I fixed it. One of the padding method I got from Pad a String with Zeros or Spaces in Java.Resurrect
F
2

I took Michal's idea above and wrapped the existing com.fasterxml.jackson.datatype.jsr310.DecimalUtils#toBigDecimal(long seconds, int nanoseconds) in a serializer

class ShortInstantSerializer extends StdSerializer<Instant> {
    ShortInstantSerializer() {
        super(Instant.class);
    }

    @Override
    public void serialize(Instant value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeNumber(DecimalUtils.toBigDecimal(value.getEpochSecond(), value.getNano()));
    }
}

Also, if you're going to be reading it back at some point, the value will be deserialized into a Double. To go back do this (thanks inverwebs!)

public static Instant toInstant(Double d) {
    long seconds = d.longValue();
    long micros = Math.round((d - seconds) * 1_000_000);
    return Instant.ofEpochSecond(seconds).plus(micros , ChronoUnit.MICROS);
}
Factitive answered 7/5, 2020 at 23:42 Comment(3)
To really achieve the short nano seconds decimal (without extra 0 at the end), you need to change the BigDecimal obtained with setScale(3)Ellora
@Patrick, by "really achieve" do you mean this does not work? This is a Jackson library used for conversion so you are saying it does not work, or am I using it wrong? Or maybe you haven't tried it?Factitive
from memory I tried it with my project and to get 1421261297.356 not 1421261297.356000000 I need to do this. Sorry it was too long I don't remember exact detailEllora

© 2022 - 2024 — McMap. All rights reserved.