Need Jackson serializer for Double and need to spe

2019-06-20 20:57发布

问题:

There are many posts about creating Jackson serializers for numbers, currency, etc. For engineering applications, there is often a need to set the precision on numbers based on the units or other criteria.

For example, spatial coordinates might be constrained to 5 or 6 digits after the decimal point, and temperature might be constrained to 2 digits after the decimal point. Default serializer behavior that has too many digits or truncated exponential notation is not good. What I need is something like:

@JsonSerialize(using=MyDoubleSerializer.class, precision=6) double myValue;

and better yet be able to specify the precision at run-time. I am also using a MixIn. I could write a serializer for each class but hoped to specify on specific values.

Any ideas would be appreciated.

回答1:

You may use Jackson's ContextualSerializer to achieve desired serialization as shown below.

Firstly, create an annotation to capture precision

@Target({ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Precision {
    int precision();
}

Next, create a contextual serializer for Double type which looks for Precision annotation on the field to be serialized and then create a new serializer for the specified precision.

public class DoubleContextualSerializer extends JsonSerializer<Double> implements ContextualSerializer {

    private int precision = 0;

    public DoubleContextualSerializer (int precision) {
        this.precision = precision;
    }

    public DoubleContextualSerializer () {

    }

    @Override
    public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        if (precision == 0) {
            gen.writeNumber(value.doubleValue());
        } else {
            BigDecimal bd = new BigDecimal(value);
            bd = bd.setScale(precision, RoundingMode.HALF_UP);
            gen.writeNumber(bd.doubleValue());
        }

    }
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        Precision precision = property.getAnnotation(Precision.class);
        if (precision != null) {
            return new DoubleContextualSerializer (precision.precision());
        }
        return this;
    }
}

Finally, annotate your field to use custom serializer and set precision

public class Bean{

   @JsonSerialize(using = DoubleContextualSerializer .class)
   @Precision(precision = 2)
   private double doubleNumber;

}

Hope this helps!!



回答2:

I used most of the suggested code but did the following, which uses DecimalFormat to do the formatting, which required outputting the raw text:

import java.io.IOException;
import java.text.DecimalFormat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;

/**
 * Custom serializer to serialize Double to a specified precision in output string.
 * The @FormatterPrecision(precision=2) annotation needs to have been specified, for example:
 * <pre>
 * @JsonSerialize(using=JacksonJsonDoubleSerializer.class) @FormatterPrecision(precision=6) abstract Double getLatitude();
 * </pre>
 * @author sam
 *
 */
public class JacksonJsonDoubleSerializer extends JsonSerializer<Double> implements ContextualSerializer {

    /**
     * Precision = number of digits after the decimal point to display.
     * Last digit will be rounded depending on the value of the next digit.
     */
    private int precision = 4;

    /**
     * Default constructor.
     */
    public JacksonJsonDoubleSerializer ( ) {

    }

    /**
     * Constructor.
     * @param precision number of digits after the decimal to format numbers.
     */
    public JacksonJsonDoubleSerializer ( int precision ) {
            this.precision = precision;
    }

    /**
     * Format to use.  Create an instance so it is shared between serialize calls.
     */
    private DecimalFormat format = null;

    /**
     *
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property ) throws JsonMappingException {
            FormatterPrecision precision = property.getAnnotation(FormatterPrecision.class);
            if ( precision != null ) {
                    return new JacksonJsonDoubleSerializer(precision.precision());
            }
            return this;
    }

    /**
     * Check that the format has been created.
     */
    private DecimalFormat getFormat () {
            if ( this.format == null ) {
                    // No format so create it
                    StringBuilder b = new StringBuilder("0.");
                    for ( int i = 0; i < this.precision; i++ ) {
                            b.append("0");
                    }
                    this.format = new DecimalFormat(b.toString());
            }
            return this.format;
    }

    /**
     * Serialize a double
     */
    @Override
    public void serialize(Double value, JsonGenerator jgen, SerializerProvider provider ) throws IOException {
            if ( (value == null) || value.isNaN() ) {
                    jgen.writeNull();
            }
            else {
                    DecimalFormat format = getFormat();
                    jgen.writeRawValue(format.format(value));
            }
    }
}

I am using a MixIn, so that class has:

public abstract class StationJacksonMixIn {

    @JsonCreator
    public StationJacksonMixIn () {

    }

    // Serializers to control formatting
    @JsonSerialize(using=JacksonJsonDoubleSerializer.class) 
    @FormatterPrecision(precision=6) abstract Double getLatitude();
    @JsonSerialize(using=JacksonJsonDoubleSerializer.class) 
    @FormatterPrecision(precision=6) abstract Double getLongitude();
}

And, finally, enable the MixIn in the ObjectMapper:

ObjectMapper objectMapper = new ObjectMapper().
                addMixIn(Station.class,StationJacksonMixIn.class);

It works well to provide a precision where it applies globally on the data field.