Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Precision lost on BigDecimals #2618

Closed
3 of 4 tasks
chosegood opened this issue Jan 9, 2020 · 6 comments
Closed
3 of 4 tasks

Precision lost on BigDecimals #2618

chosegood opened this issue Jan 9, 2020 · 6 comments
Assignees
Labels
Milestone

Comments

@chosegood
Copy link

chosegood commented Jan 9, 2020

Task List

Precision lost on BigDecimals on deserialization. Issues was encountered using Flowables (rx java) but below example also demonstrates the problem.

  • Steps to reproduce provided
  • Stacktrace (if present) provided
  • Example that reproduces the problem uploaded to Github
  • Full description of the issue provided (see below)

Steps to Reproduce

  1. mn create-app micronaut-bugreport-bigdecimal --lang=kotlin
  2. mn create-controller BugController --lang=kotlin
  3. Add jackson dependency to build.gradle
//jackson
implementation (platform("com.fasterxml.jackson:jackson-bom:$jacksonVersion"))
implementation ("com.fasterxml.jackson.core:jackson-databind")
implementation ("com.fasterxml.jackson.datatype:jackson-datatype-guava")
implementation ("com.fasterxml.jackson.datatype:jackson-datatype-jdk8")
implementation ("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation ("com.fasterxml.jackson.module:jackson-module-kotlin")
  1. Add jUnit dependency to build.gradle
    testImplementation platform("org.junit:junit-bom:$jUnitVersion")
    testImplementation "org.junit.jupiter:junit-jupiter-api"
    testImplementation "org.junit.jupiter:junit-jupiter-params"
    testImplementation "org.junit.platform:junit-platform-suite-api"
    testImplementation "org.junit.platform:junit-platform-runner"
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
  1. Add HttpClient dependency to build.gradle
    implementation("org.apache.httpcomponents:httpclient:4.5.10")
  2. Modify Controller to Accept an Post of @Body of Map
    @Post("/")
    fun post(@Body body: Map<String, Any?>) {
        this.body = body
    }

    @Get("/")
    fun get(): HttpResponse<out Any> {
        return HttpResponse.ok(body)
    }
  1. Create a test
        val httpClient = HttpClients.createDefault()

        val initialDataset = mapOf<String, Any?>(
                "string" to "string",
                "bigDecimal" to BigDecimal("888.7794538169553400000")
        )

        val postRequest = HttpPost(embeddedServer.uri.resolve("/bug"))
        postRequest.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
        postRequest.entity = GzipCompressingEntity(EntityTemplate { out -> jacksonObjectMapper().writeValue(out, initialDataset) })

        httpClient.execute(postRequest, BasicResponseHandler())

        val getRequest = HttpGet(embeddedServer.uri.resolve("/bug"))
        val getResponse = httpClient.execute(getRequest, BasicResponseHandler())
        val responseDataset = jacksonObjectMapper().readValue<Map<String, Any?>>(getResponse)
        assertThat(responseDataset, equalTo(initialDataset))

Expected Behaviour

The precision is retained between the POST/GET of the BigDecimals

Actual Behaviour

The BigDecimal is converted to a Double and precision is lost

Expected: <{string=string, bigDecimal=888.7794538169553400000}>
but: was <{string=string, bigDecimal=888.7794538169553}>

Environment Information

  • Operating System: Windows
  • Micronaut Version: 1.2.8
  • JDK Version: 12

Example Application

https://github.com/chosegood/micronaut-bugreport-bigdecimal

@croudet
Copy link
Contributor

croudet commented Jan 9, 2020

Check FasterXML/jackson-databind#1911

@jameskleeh
Copy link
Contributor

I don't think this has anything to do with Micronaut. You need to configure Jackson to modify this behavior

@graemerocher
Copy link
Contributor

Maybe we should include documentation at least

@chosegood
Copy link
Author

chosegood commented Jan 10, 2020

In the sample application referenced above the following has been configured

jackson:
  serialization-inclusion: "USE_DEFAULTS"
  generator:
    WRITE_BIGDECIMAL_AS_PLAIN: true
  deserialization:
    USE_BIG_DECIMAL_FOR_FLOATS: true

The following test passes

    @Test
    fun jacksonRetainsPrecision() {
        val objectMapper = embeddedServer.applicationContext.getBean(ObjectMapper::class.java)

        val initialDataset = mapOf<String, Any?>(
                "string" to "string",
                "bigDecimal" to BigDecimal("888.7794538169553400000")
        )

        val writeValueAsBytes = objectMapper.writeValueAsBytes(initialDataset)
        val responseDataset = objectMapper.readValue<Map<String, Any?>>(writeValueAsBytes)
        assertThat(responseDataset, equalTo(initialDataset))
    }

The following test fails

    @Test
    fun micronautRetainsPrecision() {
        val objectMapper = embeddedServer.applicationContext.getBean(ObjectMapper::class.java)
        val httpClient = HttpClients.createDefault()

        val initialDataset = mapOf<String, Any?>(
                "string" to "string",
                "bigDecimal" to BigDecimal("888.7794538169553400000")
        )

        val postRequest = HttpPost(embeddedServer.uri.resolve("/bug"))
        postRequest.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
        postRequest.entity = GzipCompressingEntity(EntityTemplate { out -> objectMapper.writeValue(out, initialDataset) })

        httpClient.execute(postRequest, BasicResponseHandler())

        val getRequest = HttpGet(embeddedServer.uri.resolve("/bug"))
        val getResponse = httpClient.execute(getRequest, BasicResponseHandler())
        val responseDataset = objectMapper.readValue<Map<String, Any?>>(getResponse)
        assertThat(responseDataset, equalTo(initialDataset))
    }

@croudet
Copy link
Contributor

croudet commented Jan 10, 2020

import java.math.BigDecimal;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Main {
    public static void main(String[] args) throws JsonProcessingException {
        var mapper = new ObjectMapper().configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
        var test = new Test(new BigDecimal("0.0000000005"));
        System.out.println(mapper.writeValueAsString(test));
    }
}

class Test {
    private final BigDecimal value;

    Test(BigDecimal value) {
        this.value = value;
    }

    @JsonFormat(shape= JsonFormat.Shape.STRING)
    public BigDecimal getValue() {
        return value;
    }
}

With jackson-databind-2.10.2 gives:
{"value":"0.0000000005"}

With jackson-databind-2.9.9.3 gives:
{"value":"5E-10"}

@chosegood
Copy link
Author

Firstly, thank you everyone for your responses and your help.

One thing I'm confused by here is that if this was a Jackson configuration issue wouldn't the unit test where the ObjectMapper is retrieved from the context also be failing with the same error in the attached application?

I think that the Micronaut JacksonProcessor is being told to parse this data as a double instead of a BigDecimal. I suspect that this may be because the JsonFactory used there is getting newed up (JsonContentProcessor) instead of supplied and losing all configured properties (like how to handle BigDecimals)?

Expected: <{string=string, bigDecimal=888.7794538169553400000}>
     but: was <{string=string, bigDecimal=888.7794538169553}>
java.lang.AssertionError: 
Expected: <{string=string, bigDecimal=888.7794538169553400000}>
     but: was <{string=string, bigDecimal=888.7794538169553}>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants