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.
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!!
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.