From 478aebe2de115758b7b073e91dcd40d5b0c81071 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Wed, 15 Jul 2026 11:37:14 +0200 Subject: [PATCH] Document InputStream ownership Clarify that JSON parsing closes input streams. Test closure after successful and failed parsing. --- .../scala/play/api/libs/json/JsonSpec.scala | 26 +++++++++++++++++-- .../main/scala/play/api/libs/json/Json.scala | 8 +++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala index c57e5dacc..d554dbcdc 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala @@ -6,6 +6,7 @@ package play.api.libs.json import com.fasterxml.jackson.core.exc.StreamConstraintsException +import java.io.ByteArrayInputStream import java.math.BigInteger import java.util.Calendar import java.util.Date @@ -33,6 +34,17 @@ class JsonSpec extends org.specs2.mutable.Specification { case class IntNumbers(long: Long, integer: Int) case class FloatNumbers(float: Float, double: Double) + private class CloseTrackingInputStream(data: Array[Byte]) extends ByteArrayInputStream(data) { + private var closed = false + + def isClosed: Boolean = closed + + override def close(): Unit = { + closed = true + super.close() + } + } + val exceedsDigitsLimit: BigDecimal = BigDecimal("9" * 1000000) val exceedsDigitsLimitNegative: BigDecimal = exceedsDigitsLimit.unary_- @@ -550,7 +562,7 @@ class JsonSpec extends org.specs2.mutable.Specification { } } - "parse from InputStream" in { + "parse from InputStream and close it" in { val js = Json.obj( "key1" -> "value1", "key2" -> true, @@ -561,11 +573,21 @@ class JsonSpec extends org.specs2.mutable.Specification { "key7" -> BigDecimal("12345678901234567890.123456789") ) ) - def stream = new java.io.ByteArrayInputStream( + val stream = new CloseTrackingInputStream( js.toString.getBytes("UTF-8") ) Json.parse(stream).mustEqual(js) + stream.isClosed.mustEqual(true) + } + + "close an InputStream when parsing fails" in { + val stream = new CloseTrackingInputStream( + """{"key": @, "remaining": true}""".getBytes("UTF-8") + ) + + Json.tryParse(stream).isFailure.mustEqual(true) + stream.isClosed.mustEqual(true) } "keep isomorphism between serialized and deserialized data" in { diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala index 9157fc8cd..25440a645 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala @@ -33,12 +33,18 @@ sealed trait JsonFacade { /** * $parseDescription (use `tryParse` to be safe). * - * @param input the InputStream to parse + * The input stream is closed after parsing, including when parsing fails. + * + * @param input the InputStream to parse and close */ def parse(input: InputStream): JsValue /** * $parseDescription. + * + * The input stream is closed after parsing, including when parsing fails. + * + * @param input the InputStream to parse and close */ def tryParse(input: InputStream): Try[JsValue]