Search code examples
javaspring-bootjacksonjava-17jackson-dataformat-csv

How can I replace decimal point per comma in Jackson CsvMapper?


I have this problem and right now I didn't found any solution. I am generating a CSV file through a Spring boot endpoint using Jackson library. Everything works properly but the decimal separator. By default it uses a dot (.) but in my country decimal separator is a comma (,) and should be exported with that character.

I search in Jackson documentation but didn't see anything like setDecimalSeparator or something like this. Another try was replace the Double fields in the class that I am exporting to String, but on this case the exportation add double quotes on these fields too.

This is the piece of code that I create to generate CSVs:

ObjectMapper map = new ObjectMapper();
byte[] data = map.writerWithDefaultPrettyPrinter().writeValueAsBytes(responseObject);
byte[] dataCsv;
if (data.length != 3) {
    JsonNode jsonTree = new ObjectMapper().readTree(data);

    CsvSchema.Builder csvSchemaBuilder = CsvSchema.builder();
    JsonNode firstObject = jsonTree.elements().next();
    firstObject.fieldNames().forEachRemaining(fieldName -> {
        csvSchemaBuilder.addColumn(fieldName);
    });

    CsvSchema csvSchema = csvSchemaBuilder.build().withHeader().withColumnSeparator(';');

    CsvMapper csvMapper = new CsvMapper();
    dataCsv = csvMapper.writerFor(JsonNode.class).with(csvSchema).writeValueAsBytes(jsonTree);
} else {
    throw new ResponseNoResultsException();
}

HttpHeaders headers = new HttpHeaders();
headers.add("Access-Control-Expose-Headers", "Content-Disposition");
headers.add("Content-Disposition", "attachment; filename=\"" + filename + ".csv\"");
headers.add("X-Frame-Options", "DENY");
headers.add("Content-Security-Policy", "default-src: 'self'; script-src: 'self' static.domain.tld");
headers.add("Strict-Transport-Security", "max-age=31536000;");
headers.add("X-Content-Type-Options", "nosniff");

return ResponseEntity
        .ok()
        .contentLength(dataCsv.length)
        .contentType(
                MediaType.parseMediaType("application/octet-stream"))
        .headers(headers)
        .body(new InputStreamResource(new ByteArrayInputStream(dataCsv)));

The class that I am returning as CSV is like this one:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonPropertyOrder({
        "field1",
        "field2",
        "field3",
        "field4",
        
})
public class ExampleQuestionClass {
    @JsonProperty("field1")
    private String field1;
    @JsonProperty("field2")
    private String field2;
    @JsonProperty("field3")
    private Double field3;
    @JsonProperty("field4")
    private Double field4;
}

And the final CSV should be like this one:

field1;field2;field3;field4;
"Value 1";"Value, 2";7,1;8,0;

Can you help me with this?


Solution

  • We could customise the serialisation of the double fields.

    1. Implement custom deserialiser
    2. annotate the field using JsonDeserializer
    public static class CustomDoubleSerializer extends StdSerializer<Double> {
    
        public CustomDoubleSerializer() {
            this(null);
        }
    
        public CustomDoubleSerializer(Class<Double> cls) {
            super(cls);
        }
    
        @Override
        public void serialize(Double value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            // German using , instead of . to separate decimal
            NumberFormat instance = NumberFormat.getInstance(Locale.GERMAN);
            instance.setMinimumFractionDigits(1);
            gen.writeString(instance.format(value));
        }
    }
    

    Annotate the class like

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonPropertyOrder({
            "field1",
            "field2",
            "field3",
            "field4",
    
    })
    public class ExampleQuestionClass {
        @JsonProperty("field1")
        private String field1;
        @JsonProperty("field2")
        private String field2;
        @JsonSerialize(using = CustomDoubleSerializer.class)
        @JsonProperty("field3")
        private Double field3;
        @JsonSerialize(using = CustomDoubleSerializer.class)
        @JsonProperty("field4")
        private Double field4;
    }
    

    Try with below main method

    public static void main(String[] args) throws IOException {
        ObjectMapper map = new ObjectMapper();
        byte[] data = map.writerWithDefaultPrettyPrinter().writeValueAsBytes(List.of(new ExampleQuestionClass("Value 1", "Value, 2", 7.1, 8.0)));
        if (data.length != 3) {
            JsonNode jsonTree = new ObjectMapper().readTree(data);
    
            CsvSchema.Builder csvSchemaBuilder = CsvSchema.builder();
            JsonNode firstObject = jsonTree.elements().next();
            firstObject.fieldNames().forEachRemaining(csvSchemaBuilder::addColumn);
    
            CsvSchema csvSchema = csvSchemaBuilder.build().withHeader().withColumnSeparator(';');
            CsvMapper csvMapper = new CsvMapper();
            System.out.println(csvMapper.writerFor(JsonNode.class).with(csvSchema).writeValueAsString(jsonTree));
        }
    }
    

    Should show

    "field1";"field2";"field3";"field4"
    "Value 1";"Value, 2";"7,1";"8,0"