Search code examples
scalaplayframeworkplay-json

Why does play-json lose precision while reading/parsing?


In the following example (scala 2.11 and play-json 2.13)

val j ="""{"t":2.2599999999999997868371792719699442386627197265625}"""
println((Json.parse(j) \ "t").as[BigDecimal].compare(BigDecimal("2.2599999999999997868371792719699442386627197265625")))

The output is -1. Shouldn't they be equal ? On printing the parsed value, it prints rounded off value:

println((Json.parse(j) \ "t").as[BigDecimal]) gives 259999999999999786837179271969944


Solution

  • The problem is that by default play-json configures the Jackson parser with the MathContext set to DECIMAL128. You can fix this by setting the play.json.parser.mathContext system property to unlimited. For example, in a Scala REPL that would look like this:

    scala> System.setProperty("play.json.parser.mathContext", "unlimited")
    res0: String = null
    
    scala> val j ="""{"t":2.2599999999999997868371792719699442386627197265625}"""
    j: String = {"t":2.2599999999999997868371792719699442386627197265625}
    
    scala> import play.api.libs.json.Json
    import play.api.libs.json.Json
    
    scala> val res = (Json.parse(j) \ "t").as[BigDecimal]
    res: BigDecimal = 2.2599999999999997868371792719699442386627197265625
    
    scala> val expected = BigDecimal("2.2599999999999997868371792719699442386627197265625")
    expected: scala.math.BigDecimal = 2.2599999999999997868371792719699442386627197265625
    
    scala> res.compare(expected)
    res1: Int = 0
    

    Note that setProperty should happen first, before any reference to Json. In normal (non-REPL) use you'd set the property via -D on the command line or whatever.

    Alternatively you could use Jawn's play-json parsing support, which just works as expected off the shelf:

    scala> val j ="""{"t":2.2599999999999997868371792719699442386627197265625}"""
    j: String = {"t":2.2599999999999997868371792719699442386627197265625}
    
    scala> import org.typelevel.jawn.support.play.Parser
    import org.typelevel.jawn.support.play.Parser
    
    scala> val res = (Parser.parseFromString(j).get \ "t").as[BigDecimal]
    res: BigDecimal = 2.2599999999999997868371792719699442386627197265625
    

    Or for that matter you could switch to circe:

    scala> import io.circe.Decoder, io.circe.jawn.decode
    import io.circe.Decoder
    import io.circe.jawn.decode
    
    scala> decode(j)(Decoder[BigDecimal].prepare(_.downField("t")))
    res0: Either[io.circe.Error,BigDecimal] = Right(2.2599999999999997868371792719699442386627197265625)
    

    …which handles a range of number-related corner cases more responsibly than play-json in my view. For example:

    scala> val big = "1e2147483648"
    big: String = 1e2147483648
    
    scala> io.circe.jawn.parse(big)
    res0: Either[io.circe.ParsingFailure,io.circe.Json] = Right(1e2147483648)
    
    scala> play.api.libs.json.Json.parse(big)
    java.lang.NumberFormatException
      at java.math.BigDecimal.<init>(BigDecimal.java:491)
      at java.math.BigDecimal.<init>(BigDecimal.java:824)
      at scala.math.BigDecimal$.apply(BigDecimal.scala:287)
      at play.api.libs.json.jackson.JsValueDeserializer.parseBigDecimal(JacksonJson.scala:146)
      ...
    

    But that's out of scope for this question.

    To be honest I'm not sure why play-json defaults to DECIMAL128 for the MathContext, but that's a question for the play-json maintainers, and is also out of scope here.