'Need Jackson serializer for Double and need to specify precision at runtime

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.



Solution 1:[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!!

Solution 2:[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.

Solution 3:[3]

After my initial more complex attempt, I found a simpler solution that works well. This involves using BigDecimal where appropriate only for output formatting. For example, the following code has getValueFormatted method that formats the value to the correct number of digits. In this case, the output JSON uses a short element name v rather than value and @JsonProperty("v") is used to indicate that the getValueFormatted method should be used to return the value for output. I did not put in a MixIn but could do that. This adds a method to the normal object but that is OK for REST service code.

  /**
   * Return the data value.
   * Ignored for JSON output because use getValueFormatted instead.
   */
  @JsonIgnore
  public Double getValue () {
    return this.value;
  }

  /**
   * Return the value formatted to appropriate number of digits.
   * This is used with serialization to output.
   * Use for JSON output because it is formatted to the correct number of digits.
   * @return the data value formatted to the appropriate number of digits.
   */
  @JsonProperty("v")
  public BigDecimal getValueFormatted () {
    // BigDecimal does not handle the concept of NaN and therefore can't serialize those values as Double does.
    if ( (this.value == null) || this.value.isNaN() || this.value.isInfinite() ) {
      return null;
    }
    BigDecimal bd = new BigDecimal(this.value).setScale(this.valueDigits,RoundingMode.HALF_UP);
    return bd;
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 smalers
Solution 3 smalers