diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/ReadsSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/ReadsSpec.scala index 140eda494..96b2ea4ce 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/ReadsSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/ReadsSpec.scala @@ -48,53 +48,43 @@ class ReadsSpec extends org.specs2.mutable.Specification { ) "be successfully read from number" in { - reads(JsNumber(JBigDec.valueOf(123L))) - .aka("read date") - .must_==( - JsSuccess( - LocalDateTime.ofInstant( - Instant.ofEpochMilli(123L), - ZoneOffset.UTC - ) + reads(JsNumber(JBigDec.valueOf(123L))).must_===( + JsSuccess( + LocalDateTime.ofInstant( + Instant.ofEpochMilli(123L), + ZoneOffset.UTC ) ) + ) } "not be read from invalid number" in { - reads(JsNumber(BigDecimal("1000000000e1000000000"))) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok - }) + reads(JsNumber(BigDecimal("1000000000e1000000000"))).must(beLike { + case JsError.Message("error.expected.long") => ok + }) } "not be read from invalid string" in { - reads(JsString("invalid")) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok - }) + reads(JsString("invalid")).must(beLike { + case JsError.Message("error.expected.date.isoformat") => ok + }) } "be successfully read with default implicit" >> { "from '2011-12-03T10:15:30'" in { - reads(JsString("2011-12-03T10:15:30")).aka("read date").must_==(JsSuccess(dateTime("2011-12-03T10:15:30"))) + reads(JsString("2011-12-03T10:15:30")).aka("read date").must_===(JsSuccess(dateTime("2011-12-03T10:15:30"))) } "from '2011-12-03T10:15:30+01:00' (with TZ offset)" in { reads(JsString("2011-12-03T10:15:30+01:00")) .aka("read date") - .must_==( - JsSuccess(dateTime("2011-12-03T10:15:30+01:00")) - ) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30+01:00"))) } "from '2011-12-03T10:15:30+01:00[Europe/Paris]' (with time zone)" in { reads(JsString("2011-12-03T10:15:30+01:00[Europe/Paris]")) .aka("read date") - .must_==( - JsSuccess(dateTime("2011-12-03T10:15:30+01:00[Europe/Paris]")) - ) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30+01:00[Europe/Paris]"))) } } @@ -102,7 +92,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads1 .reads(JsString("03/12/2011, 10:15:30")) .aka("read date") - .must_==(JsSuccess(dateTime("2011-12-03T10:15:30"))) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30"))) } "not be read from invalid corrected string" >> { @@ -110,7 +100,8 @@ class ReadsSpec extends org.specs2.mutable.Specification { correctedReads .reads(JsString("2011-12-03T10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message(key) => + key.must_===("error.expected.date.isoformat") }) } @@ -118,7 +109,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads2 .reads(JsString("03/12/2011, 10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } } @@ -127,11 +118,11 @@ class ReadsSpec extends org.specs2.mutable.Specification { lazy val time = dateTime("2011-12-03T10:15:30") "with default implicit" in { - correctedReads.reads(JsString("_2011-12-03T10:15:30")).aka("read date").must_==(JsSuccess(time)) + correctedReads.reads(JsString("_2011-12-03T10:15:30")).aka("read date").must_===(JsSuccess(time)) } "with custom formatter" in { - CustomReads2.reads(JsString("# 03/12/2011, 10:15:30")).aka("read date").must_==(JsSuccess(time)) + CustomReads2.reads(JsString("# 03/12/2011, 10:15:30")).aka("read date").must_===(JsSuccess(time)) } } } @@ -156,19 +147,15 @@ class ReadsSpec extends org.specs2.mutable.Specification { "not be read" >> { "from an invalid string" in { - reads(JsString("invalid")) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok - }) + reads(JsString("invalid")).must(beLike { + case JsError.Message("error.expected.date.isoformat") => ok + }) } "from a number" in { - reads(JsNumber(123L)) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date" :: Nil) :: Nil) :: Nil) => ok - }) + reads(JsNumber(123L)).must(beLike { + case JsError.Message("error.expected.date") => ok + }) } } @@ -176,7 +163,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { "from '2011-12-03T10:15:30-05:00'" in { reads(JsString("2011-12-03T10:15:30-05:00")) .aka("read date") - .must_==(JsSuccess(dateTime("2011-12-03T10:15:30-05:00"))) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30-05:00"))) } } @@ -184,7 +171,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads1 .reads(JsString("03/12/2011, 10:15:30-05:00")) .aka("read date") - .must_==(JsSuccess(dateTime("2011-12-03T10:15:30-05:00"))) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30-05:00"))) } "not be read from invalid corrected string" >> { @@ -192,7 +179,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { correctedReads .reads(JsString("2011-12-03T10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } @@ -200,7 +187,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads2 .reads(JsString("03/12/2011, 10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } } @@ -209,11 +196,11 @@ class ReadsSpec extends org.specs2.mutable.Specification { lazy val time = dateTime("2011-12-03T10:15:30-05:00") "with default implicit" in { - correctedReads.reads(JsString("_2011-12-03T10:15:30-05:00")).aka("read date").must_==(JsSuccess(time)) + correctedReads.reads(JsString("_2011-12-03T10:15:30-05:00")).aka("read date").must_===(JsSuccess(time)) } "with custom formatter" in { - CustomReads2.reads(JsString("# 03/12/2011, 10:15:30 -0500")).aka("read date").must_==(JsSuccess(time)) + CustomReads2.reads(JsString("# 03/12/2011, 10:15:30 -0500")).aka("read date").must_===(JsSuccess(time)) } } } @@ -237,31 +224,27 @@ class ReadsSpec extends org.specs2.mutable.Specification { ) "be successfully read from number" in { - reads(JsNumber(JBigDec.valueOf(123L))) - .aka("read date") - .must_==( - JsSuccess( - ZonedDateTime.ofInstant( - Instant.ofEpochMilli(123L), - ZoneOffset.UTC - ) + reads(JsNumber(JBigDec.valueOf(123L))).must_===( + JsSuccess( + ZonedDateTime.ofInstant( + Instant.ofEpochMilli(123L), + ZoneOffset.UTC ) ) + ) } "not be read from invalid string" in { - reads(JsString("invalid")) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok - }) + reads(JsString("invalid")).must(beLike { + case JsError.Message("error.expected.date.isoformat") => ok + }) } "not be read from invalid number" in { reads(JsNumber(BigDecimal("1000000000e1000000000"))) .aka("read date") .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.long") => ok }) } @@ -269,17 +252,13 @@ class ReadsSpec extends org.specs2.mutable.Specification { "from '2011-12-03T10:15:30+01:00' (with TZ offset)" in { reads(JsString("2011-12-03T10:15:30+01:00")) .aka("read date") - .must_==( - JsSuccess(dateTime("2011-12-03T10:15:30+01:00")) - ) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30+01:00"))) } "from '2011-12-03T10:15:30+01:00[Europe/Paris]' (with time zone)" in { reads(JsString("2011-12-03T10:15:30+01:00[Europe/Paris]")) .aka("read date") - .must_==( - JsSuccess(dateTime("2011-12-03T10:15:30+01:00[Europe/Paris]")) - ) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30+01:00[Europe/Paris]"))) } } @@ -287,7 +266,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads1 .reads(JsString("03/12/2011, 10:15:30+08:00")) .aka("read date") - .must_==(JsSuccess(dateTime("2011-12-03T10:15:30+08:00"))) + .must_===(JsSuccess(dateTime("2011-12-03T10:15:30+08:00"))) } "not be read from invalid corrected string" >> { @@ -295,7 +274,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { correctedReads .reads(JsString("2011-12-03T10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } @@ -303,7 +282,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads2 .reads(JsString("03/12/2011, 10:15:30+08:00")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } } @@ -312,11 +291,11 @@ class ReadsSpec extends org.specs2.mutable.Specification { lazy val time = dateTime("2011-12-03T10:15:30+08:00") "with default implicit" in { - correctedReads.reads(JsString("_2011-12-03T10:15:30+08:00")).aka("read date").must_==(JsSuccess(time)) + correctedReads.reads(JsString("_2011-12-03T10:15:30+08:00")).aka("read date").must_===(JsSuccess(time)) } "with custom formatter" in { - CustomReads2.reads(JsString("# 03/12/2011, 10:15:30+08:00")).aka("read date").must_==(JsSuccess(time)) + CustomReads2.reads(JsString("# 03/12/2011, 10:15:30+08:00")).aka("read date").must_===(JsSuccess(time)) } } } @@ -343,31 +322,27 @@ class ReadsSpec extends org.specs2.mutable.Specification { val beforeMidnight = Instant.parse("1970-01-01T23:55:00Z") val d = LocalDate.parse("1970-01-01") - reads(JsNumber(JBigDec.valueOf(beforeMidnight.toEpochMilli))).aka("read date").must_==(JsSuccess(d)) + reads(JsNumber(JBigDec.valueOf(beforeMidnight.toEpochMilli))).aka("read date").must_===(JsSuccess(d)) } "not be read from invalid number" in { - reads(JsNumber(BigDecimal("1000000000e1000000000"))) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok - }) + reads(JsNumber(BigDecimal("1000000000e1000000000"))).must(beLike { + case JsError.Message("error.expected.long") => ok + }) } "not be read from invalid string" in { - reads(JsString("invalid")) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok - }) + reads(JsString("invalid")).must(beLike { + case JsError.Message("error.expected.date.isoformat") => ok + }) } "be successfully read with default implicit from '2011-12-03'" in { - reads(JsString("2011-12-03")).aka("read date").must_==(JsSuccess(date("2011-12-03"))) + reads(JsString("2011-12-03")).must_===(JsSuccess(date("2011-12-03"))) } "be successfully read with custom pattern from '03/12/2011'" in { - CustomReads1.reads(JsString("03/12/2011")).aka("read date").must_==(JsSuccess(date("2011-12-03"))) + CustomReads1.reads(JsString("03/12/2011")).aka("read date").must_===(JsSuccess(date("2011-12-03"))) } "not be read from invalid corrected string" >> { @@ -375,7 +350,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { correctedReads .reads(JsString("2011-12-03")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } @@ -383,7 +358,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads2 .reads(JsString("03/12/2011")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } } @@ -392,11 +367,11 @@ class ReadsSpec extends org.specs2.mutable.Specification { lazy val d = date("2011-12-03") "with default implicit" in { - correctedReads.reads(JsString("_2011-12-03")).aka("read date").must_==(JsSuccess(d)) + correctedReads.reads(JsString("_2011-12-03")).aka("read date").must_===(JsSuccess(d)) } "with custom formatter" in { - CustomReads2.reads(JsString("# 03/12/2011")).aka("read date").must_==(JsSuccess(d)) + CustomReads2.reads(JsString("# 03/12/2011")).must_===(JsSuccess(d)) } } } @@ -420,31 +395,27 @@ class ReadsSpec extends org.specs2.mutable.Specification { "be successfully read from number" in { val d = LocalTime.parse("10:15") - reads(JsNumber(BigDecimal.valueOf(d.toNanoOfDay))).aka("read time").must_==(JsSuccess(d)) + reads(JsNumber(BigDecimal.valueOf(d.toNanoOfDay))).must_===(JsSuccess(d)) } "not be read from invalid number" in { - reads(JsNumber(BigDecimal("1000000000e1000000000"))) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok - }) + reads(JsNumber(BigDecimal("1000000000e1000000000"))).must(beLike { + case JsError.Message("error.expected.long") => ok + }) } "not be read from invalid string" in { - reads(JsString("invalid")) - .aka("read time") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok - }) + reads(JsString("invalid")).must(beLike { + case JsError.Message("error.expected.date.isoformat") => ok + }) } "be successfully read with default implicit from '10:15:30'" in { - reads(JsString("10:15:30")).must_==(JsSuccess(LocalTime.of(10, 15, 30))) + reads(JsString("10:15:30")).must_===(JsSuccess(LocalTime.of(10, 15, 30))) } "be successfully read with custom pattern from '10.15.30'" in { - CustomReads1.reads(JsString("10.15.30")).aka("read time").must_==(JsSuccess(LocalTime.of(10, 15, 30))) + CustomReads1.reads(JsString("10.15.30")).aka("read time").must_===(JsSuccess(LocalTime.of(10, 15, 30))) } "not be read from invalid corrected string" >> { @@ -452,7 +423,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { correctedReads .reads(JsString("0:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } @@ -460,7 +431,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads2 .reads(JsString("10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } } @@ -469,11 +440,11 @@ class ReadsSpec extends org.specs2.mutable.Specification { lazy val d = LocalTime.of(10, 15, 30) "with default implicit" in { - correctedReads.reads(JsString("_10:15:30")).must_==(JsSuccess(d)) + correctedReads.reads(JsString("_10:15:30")).must_===(JsSuccess(d)) } "with custom formatter" in { - CustomReads2.reads(JsString("# 10.15.30")).must_==(JsSuccess(d)) + CustomReads2.reads(JsString("# 10.15.30")).must_===(JsSuccess(d)) } } } @@ -495,36 +466,30 @@ class ReadsSpec extends org.specs2.mutable.Specification { ) "be successfully read from number" in { - reads(JsNumber(JBigDec.valueOf(123L))).aka("read date").must_==(JsSuccess(Instant.ofEpochMilli(123L))) + reads(JsNumber(JBigDec.valueOf(123L))).must_===(JsSuccess(Instant.ofEpochMilli(123L))) } "not be read from invalid number" in { reads(JsNumber(BigDecimal("1000000000e1000000000"))) .aka("read date") .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.long") => ok }) } "not be read from invalid string" in { - reads(JsString("invalid")) - .aka("read date") - .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok - }) + reads(JsString("invalid")).must(beLike { + case JsError.Message("error.expected.date.isoformat") => ok + }) } "be successfully read with default implicit" >> { "from '2015-05-01T13:00:00Z' (with zeros)" in { - reads(JsString("2015-05-01T00:00:00Z")) - .aka("read data") - .must_==(JsSuccess(Instant.parse("2015-05-01T00:00:00Z"))) + reads(JsString("2015-05-01T00:00:00Z")).must_===(JsSuccess(Instant.parse("2015-05-01T00:00:00Z"))) } "from '2011-12-03T10:15:30Z'" in { - reads(JsString("2011-12-03T10:15:30Z")) - .aka("read date") - .must_==(JsSuccess(Instant.parse("2011-12-03T10:15:30Z"))) + reads(JsString("2011-12-03T10:15:30Z")).must_===(JsSuccess(Instant.parse("2011-12-03T10:15:30Z"))) } "from '2015-05-01T13:00:00+02:00' (with TZ offset and zeros)" in { @@ -572,7 +537,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { correctedReads .reads(JsString("2011-12-03T10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } @@ -580,7 +545,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { CustomReads2 .reads(JsString("03/12/2011, 10:15:30")) .must(beLike { - case JsError((_, JsonValidationError("error.expected.date.isoformat" :: Nil, _) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.date.isoformat") => ok }) } } @@ -606,26 +571,26 @@ class ReadsSpec extends org.specs2.mutable.Specification { Fragment.foreach(validTimeZones)(tz => s"be successfully read from $tz" in { - reads(JsString(tz)).aka("read ZoneId").must_==(JsSuccess(ZoneId.of(tz))) + reads(JsString(tz)).must_===(JsSuccess(ZoneId.of(tz))) } ) "not be read from number" in { val reads1 = reads(JsNumber(123)) reads1.must(beLike { - case JsError((_, JsonValidationError("error.expected.jsstring" :: Nil) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.jsstring") => ok }) } "not be read from unknown time zone" in { reads(JsString("America/Gotham")).must(beLike { - case JsError((_, JsonValidationError("error.expected.timezone" :: Nil, "America/Gotham") :: Nil) :: Nil) => ok + case JsError.Detailed("error.expected.timezone", "America/Gotham") => ok }) } "not be read from malformed time zone" in { reads(JsString("UTC+x")).must(beLike { - case JsError((_, JsonValidationError("error.expected.timezone" :: Nil, "UTC+x") :: Nil) :: Nil) => ok + case JsError.Detailed("error.expected.timezone", "UTC+x") => ok }) } } @@ -721,18 +686,18 @@ class ReadsSpec extends org.specs2.mutable.Specification { import DefaultReads.reads "parse a long number" in { - reads(JsNumber(JBigDec.valueOf(123L))).aka("read long number").must_==(JsSuccess(123L)) + reads(JsNumber(JBigDec.valueOf(123L))).aka("read long number").must_===(JsSuccess(123L)) } "parse a negative long number" in { - reads(JsNumber(JBigDec.valueOf(-123L))).aka("read long number").must_==(JsSuccess(-123L)) + reads(JsNumber(JBigDec.valueOf(-123L))).aka("read long number").must_===(JsSuccess(-123L)) } "not read from invalid number" in { reads(JsNumber(BigDecimal("1000000000e1000000000"))) .aka("read long number") .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.long") => ok }) } @@ -740,7 +705,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { reads(JsNumber(veryLargeNumber)) .aka("read long number") .must(beLike { - case JsError((_, JsonValidationError("error.expected.long" :: Nil) :: Nil) :: Nil) => ok + case JsError.Message("error.expected.long") => ok }) } } @@ -750,7 +715,7 @@ class ReadsSpec extends org.specs2.mutable.Specification { Json .obj("en" -> 1, "fr" -> 2) .validate[Map[Locale, Int]] - .must_==(JsSuccess(Map(Locale.ENGLISH -> 1, Locale.FRENCH -> 2))) + .must_===(JsSuccess(Map(Locale.ENGLISH -> 1, Locale.FRENCH -> 2))) } } } diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsResult.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsResult.scala index 1a1fbed5f..765ef1bed 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsResult.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsResult.scala @@ -159,6 +159,44 @@ object JsError { case js: JsValue => js case x => JsString(x.toString) } + + /** + * Extracts the first error message. + * + * {{{ + * import play.api.libs.json.JsError + * + * def msg(err: JsError): Option[String] = err match { + * case JsError.Message(msg) => Some(msg) + * case _ => None + * } + * }}} + */ + object Message { + def unapply(error: JsError): Option[String] = + error.errors.headOption.collect { + case (_, JsonValidationError.Message(msg) +: _) => msg + } + } + + /** + * Extracts the first error details (message and its first argument). + * + * {{{ + * import play.api.libs.json.JsError + * + * def cause(err: JsError): Option[(String, Exception)] = err match { + * case JsError.Detailed(msg, ex: Exception) => Some(msg -> ex) + * case _ => None + * } + * }}} + */ + object Detailed { + def unapply(error: JsError): Option[(String, Any)] = + error.errors.headOption.collect { + case (_, JsonValidationError.Detailed(msg, arg) +: _) => msg -> arg + } + } } sealed trait JsResult[+A] { self => diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsonValidationError.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsonValidationError.scala index 36bfc0a77..02c80623e 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsonValidationError.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsonValidationError.scala @@ -12,8 +12,55 @@ case class JsonValidationError(messages: Seq[String], args: Any*) { } object JsonValidationError { + + /** + * {{{ + * import play.api.libs.json.JsonValidationError + * + * val simpleError = JsonValidationError("error.key1") + * + * val detailedError = JsonValidationError("error.key1", "details") + * }}} + */ def apply(message: String, args: Any*): JsonValidationError = JsonValidationError(Seq(message), args: _*) private[json] val PathMissing = Seq(JsonValidationError("error.path.missing")) + + /** + * Extracts the first error message. + * + * {{{ + * import play.api.libs.json.JsonValidationError + * + * def msg(err: JsonValidationError): Option[String] = err match { + * case JsonValidationError.Message(msg) => Some(msg) + * case _ => None + * } + * }}} + */ + object Message { + def unapply(error: JsonValidationError): Option[String] = + error.messages.headOption + } + + /** + * Extracts the first error details (message and its first argument). + * + * {{{ + * import play.api.libs.json.JsonValidationError + * + * def details(err: JsonValidationError): Option[(String, Any)] = err match { + * case JsonValidationError.Detailed(msg, arg) => Some(msg -> arg) + * case _ => None + * } + * }}} + */ + object Detailed { + def unapply(error: JsonValidationError): Option[(String, Any)] = + for { + msg <- error.messages.headOption + arg <- error.args.headOption + } yield msg -> arg + } } diff --git a/play-json/shared/src/test/scala/play/api/libs/json/JsonValidSharedSpec.scala b/play-json/shared/src/test/scala/play/api/libs/json/JsonValidSharedSpec.scala index da2d60479..9f81819f0 100644 --- a/play-json/shared/src/test/scala/play/api/libs/json/JsonValidSharedSpec.scala +++ b/play-json/shared/src/test/scala/play/api/libs/json/JsonValidSharedSpec.scala @@ -241,11 +241,7 @@ class JsonValidSharedSpec extends AnyWordSpec with Matchers { "error" } mustEqual "error" - JsNumber(123) - .validate[Int] - .recoverTotal { _ => - 0 - } mustEqual 123 + JsNumber(123).validate[Int].recoverTotal(_ => 0) mustEqual 123 } } diff --git a/play-json/shared/src/test/scala/play/api/libs/json/ReadsSharedSpec.scala b/play-json/shared/src/test/scala/play/api/libs/json/ReadsSharedSpec.scala index f17a7a3e4..e812ff0fe 100644 --- a/play-json/shared/src/test/scala/play/api/libs/json/ReadsSharedSpec.scala +++ b/play-json/shared/src/test/scala/play/api/libs/json/ReadsSharedSpec.scala @@ -12,7 +12,7 @@ import org.scalatest._ import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -final class ReadsSharedSpec extends AnyWordSpec with Matchers { +final class ReadsSharedSpec extends AnyWordSpec with Matchers with Inside { "Reads" should { "not repath the second result on flatMap" when { val aPath = JsPath \ "a" @@ -205,20 +205,8 @@ final class ReadsSharedSpec extends AnyWordSpec with Matchers { "not be read from invalid JsString" in { val strRepr = " invalid" - JsString(strRepr).validate[URI] match { - case JsError( - List( - ( - JsPath, - List( - JsonValidationError(List(msg)) - ) - ) - ) - ) => - msg.must(include("invalid")) - - case _ => () + inside(JsString(strRepr).validate[URI]) { + case JsError.Message(msg) => msg.must(include("invalid")) } } }