Skip to content

Commit

Permalink
Fix decoding of "Infinity", "-Infinity", and "NaN" for Double and `…
Browse files Browse the repository at this point in the history
…Float` values (#1246)
  • Loading branch information
plokhotnyuk authored Jan 25, 2025
1 parent 794aad8 commit b1aaf9c
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ And bad JSON will produce an error in `jq` syntax with an additional piece of co

```
scala> """{"curvature": womp}""".fromJson[Banana]
val res: Either[String, Banana] = Left(.curvature(expected a number, got w))
val res: Either[String, Banana] = Left(.curvature(expected a Double))
```

Say we extend our data model to include more data types
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ And bad JSON will produce an error in `jq` syntax with an additional piece of co

```
scala> """{"curvature": womp}""".fromJson[Banana]
val res: Either[String, Banana] = Left(.curvature(expected a number, got w))
val res: Either[String, Banana] = Left(.curvature(expected a Double))
```

Say we extend our data model to include more data types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,11 @@ object DecoderPlatformSpecificSpec extends ZIOSpecDefault {
test("test hand-coded alternative in `orElse` comment") {
val decoder: JsonDecoder[AnyVal] = JsonDecoder.peekChar[AnyVal] {
case 't' | 'f' => JsonDecoder[Boolean].widen
case c => JsonDecoder[Int].widen
case _ => JsonDecoder[Int].widen
}
assert(decoder.decodeJson("true"))(equalTo(Right(true.asInstanceOf[AnyVal]))) &&
assert(decoder.decodeJson("42"))(equalTo(Right(42.asInstanceOf[AnyVal]))) &&
assert(decoder.decodeJson("\"a string\""))(equalTo(Left("(expected a number, got 'a')")))
assert(decoder.decodeJson("\"a string\""))(equalTo(Left("(expected an Int)")))
}
)
)
Expand Down
33 changes: 16 additions & 17 deletions zio-json/shared/src/main/scala/zio/json/internal/lexer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ object Lexer {
}

def byte(trace: List[JsonError], in: RetractReader): Byte = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.byte_(in, false)
in.retract()
Expand All @@ -297,7 +298,8 @@ object Lexer {
}

def short(trace: List[JsonError], in: RetractReader): Short = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.short_(in, false)
in.retract()
Expand All @@ -308,7 +310,8 @@ object Lexer {
}

def int(trace: List[JsonError], in: RetractReader): Int = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.int_(in, false)
in.retract()
Expand All @@ -319,7 +322,8 @@ object Lexer {
}

def long(trace: List[JsonError], in: RetractReader): Long = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.long_(in, false)
in.retract()
Expand All @@ -333,7 +337,8 @@ object Lexer {
trace: List[JsonError],
in: RetractReader
): java.math.BigInteger = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits)
in.retract()
Expand All @@ -344,7 +349,8 @@ object Lexer {
}

def float(trace: List[JsonError], in: RetractReader): Float = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.float_(in, false, NumberMaxBits)
in.retract()
Expand All @@ -355,7 +361,8 @@ object Lexer {
}

def double(trace: List[JsonError], in: RetractReader): Double = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.double_(in, false, NumberMaxBits)
in.retract()
Expand All @@ -369,7 +376,8 @@ object Lexer {
trace: List[JsonError],
in: RetractReader
): java.math.BigDecimal = {
checkNumber(trace, in)
in.nextNonWhitespace()
in.retract()
try {
val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits)
in.retract()
Expand All @@ -379,15 +387,6 @@ object Lexer {
}
}

// really just a way to consume the whitespace
private def checkNumber(trace: List[JsonError], in: RetractReader): Unit = {
(in.nextNonWhitespace(): @switch) match {
case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => ()
case c => error("a number,", c, trace)
}
in.retract()
}

// optional whitespace and then an expected character
@inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = {
val got = in.nextNonWhitespace()
Expand Down
58 changes: 53 additions & 5 deletions zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,61 @@ object DecoderSpec extends ZIOSpecDefault {
val spec: Spec[Environment, Any] =
suite("Decoder")(
suite("fromJson")(
test("byte") {
assert("-123".fromJson[Byte])(isRight(equalTo(-123: Byte))) &&
assert("\"-123\"".fromJson[Byte])(isRight(equalTo(-123: Byte))) &&
assertTrue("\"Infinity\"".fromJson[Byte].isLeft) &&
assertTrue("\"-Infinity\"".fromJson[Byte].isLeft) &&
assertTrue("\"NaN\"".fromJson[Byte].isLeft)
},
test("short") {
assert("-12345".fromJson[Short])(isRight(equalTo(-12345: Short))) &&
assert("\"-12345\"".fromJson[Short])(isRight(equalTo(-12345: Short))) &&
assertTrue("\"Infinity\"".fromJson[Short].isLeft) &&
assertTrue("\"-Infinity\"".fromJson[Short].isLeft) &&
assertTrue("\"NaN\"".fromJson[Short].isLeft)
},
test("int") {
assert("-1234567890".fromJson[Int])(isRight(equalTo(-1234567890))) &&
assert("\"-1234567890\"".fromJson[Int])(isRight(equalTo(-1234567890))) &&
assertTrue("\"Infinity\"".fromJson[Int].isLeft) &&
assertTrue("\"-Infinity\"".fromJson[Int].isLeft) &&
assertTrue("\"NaN\"".fromJson[Int].isLeft)
},
test("long") {
assert("-123456789012345678".fromJson[Long])(isRight(equalTo(-123456789012345678L))) &&
assert("\"-123456789012345678\"".fromJson[Long])(isRight(equalTo(-123456789012345678L))) &&
assertTrue("\"Infinity\"".fromJson[Long].isLeft) &&
assertTrue("\"-Infinity\"".fromJson[Long].isLeft) &&
assertTrue("\"NaN\"".fromJson[Long].isLeft)
},
test("float") {
assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) &&
assert("\"-1.234567e9\"".fromJson[Float])(isRight(equalTo(-1.234567e9f))) &&
assert("\"Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) &&
assert("\"-Infinity\"".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) &&
assertTrue("\"NaN\"".fromJson[Float].isRight)
},
test("double") {
assert("-1.23456789012345e9".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) &&
assert("\"-1.23456789012345e9\"".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) &&
assert("\"Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) &&
assert("\"-Infinity\"".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) &&
assertTrue("\"NaN\"".fromJson[Double].isRight)
},
test("BigDecimal") {
assert("123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal(123))))
assert("123.0e123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal("123.0e123")))) &&
assertTrue("\"Infinity\"".fromJson[BigDecimal].isLeft) &&
assertTrue("\"-Infinity\"".fromJson[BigDecimal].isLeft) &&
assertTrue("\"NaN\"".fromJson[BigDecimal].isLeft)
},
test("256 bit BigInteger") {
assert("170141183460469231731687303715884105728".fromJson[java.math.BigInteger])(
test("BigInteger") {
assert("170141183460469231731687303715884105728".fromJson[BigInteger])(
isRight(equalTo(new BigInteger("170141183460469231731687303715884105728")))
)
) &&
assertTrue("\"Infinity\"".fromJson[BigInteger].isLeft) &&
assertTrue("\"-Infinity\"".fromJson[BigInteger].isLeft) &&
assertTrue("\"NaN\"".fromJson[BigInteger].isLeft)
},
test("BigInteger too large") {
// this big integer consumes more than 256 bits
Expand Down Expand Up @@ -56,7 +104,7 @@ object DecoderSpec extends ZIOSpecDefault {
},
test("tuples") {
assert("""["a",3]""".fromJson[(String, Int)])(isRight(equalTo(("a", 3))))
assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected a number, got 'b')")))
assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected an Int)")))
assert("""[[0.1,0.2],[0.3,0.4],[-0.3,-]]""".fromJson[Seq[(Double, Double)]])(
isLeft(equalTo("[2][1](expected a Double)"))
)
Expand Down

0 comments on commit b1aaf9c

Please sign in to comment.