Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait EnvFormatRepresentation { self: Format.Representation.type =>
private[json] trait EnvRepresentations { scope: Format.Representation.Implicits.type => }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

import java.util.Locale

import java.time.{
Instant,
LocalDate,
LocalDateTime,
LocalTime,
OffsetDateTime,
OffsetTime,
ZoneId,
ZonedDateTime,
Duration => JDuration
}

import scala.concurrent.duration.FiniteDuration

private[json] trait EnvFormatRepresentation { self: Format.Representation.type =>
import Format.Representation

implicit val zoneId: Representation[ZoneId, JsString] =
asString[ZoneId]

implicit val locale: Representation[Locale, JsString] =
asString[Locale]

// `Representation` instances that can be optionally be imported in the scope
// according to the selected `Reads`/`Writes`.
private[json] trait EnvRepresentations { scope: Format.Representation.Implicits.type =>
implicit val localDateTime: Representation[LocalDateTime, JsString] =
asString[LocalDateTime]

implicit val localDate: Representation[LocalDate, JsString] =
asString[LocalDate]

implicit val localTime: Representation[LocalTime, JsString] =
asString[LocalTime]

implicit val offsetDateTime: Representation[OffsetDateTime, JsString] =
asString[OffsetDateTime]

implicit val offsetTime: Representation[OffsetTime, JsString] =
asString[OffsetTime]

implicit val zonedDateTime: Representation[ZonedDateTime, JsString] =
asString[ZonedDateTime]

implicit val instant: Representation[Instant, JsString] =
asString[Instant]

implicit val javaDuration: Representation[JDuration, JsString] =
asString[JDuration]

implicit val finiteDuration: Representation[FiniteDuration, JsString] =
asString[FiniteDuration]
}
}
57 changes: 57 additions & 0 deletions play-json/jvm/src/test/scala/play/api/libs/json/FormatSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

import java.time.{
Instant,
LocalDateTime,
LocalDate,
LocalTime,
OffsetTime,
OffsetDateTime,
ZonedDateTime,
ZoneOffset,
ZoneId
}

final class FormatSpec extends org.specs2.mutable.Specification {
"JSON Format".title

"Representation" should {
import Format.Representation

"be found" in {
implicitly[Representation[ZoneId, JsString]]

implicitly[Representation[java.util.Locale, JsString]]

ok
}

"only be found from optional Implicits._" in {
import Representation.Implicits._

implicitly[Representation[LocalDateTime, JsString]]

implicitly[Representation[LocalDate, JsString]]

implicitly[Representation[LocalTime, JsString]]

implicitly[Representation[OffsetDateTime, JsString]]

implicitly[Representation[OffsetTime, JsString]]

implicitly[Representation[ZonedDateTime, JsString]]

implicitly[Representation[Instant, JsString]]

implicitly[Representation[java.time.Duration, JsString]]

implicitly[Representation[scala.concurrent.duration.FiniteDuration, JsString]]

ok
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait EnvFormatRepresentation { self: Format.Representation.type =>
private[json] trait EnvRepresentations { scope: Format.Representation.Implicits.type => }
}
37 changes: 37 additions & 0 deletions play-json/shared/src/main/scala/play/api/libs/json/Format.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,43 @@ object Format extends PathFormat with ConstraintFormat with DefaultFormat with S
def reads(json: JsValue) = fjs.reads(json)
def writes(o: A) = tjs.writes(o)
}

/**
* Evidence that values of type `T` are represented as values of type `R`.
*
* This type carries no runtime information and is only used to provide
* compile-time evidence of the representation.
*
* @tparam T the represented type
* @tparam R the representation type
*/
@implicitNotFound(
"No JSON representation found for ${T} as ${R}. An implicit Format.Representation[${T}, ${R}] is required."
)
trait Representation[T, R <: JsValue]

object Representation extends EnvFormatRepresentation {
private object Unsafe extends Representation[Nothing, JsValue] {}

def asString[T]: Representation[T, JsString] =
Unsafe.asInstanceOf[Representation[T, JsString]]

implicit val uriReadsRepresentation: Representation[java.net.URI, JsString] =
asString[java.net.URI]

implicit val uuidReadsRepresentation: Representation[java.util.UUID, JsString] =
asString[java.util.UUID]

/**
* Provides [[Representation]] instances for types that may be represented
* as JSON strings depending on the [[Writes]] instance in scope.
*
* These instances are not available implicitly by default and must be
* explicitly imported when a string representation is required, for example
* when using such types as JSON object keys.
*/
object Implicits extends EnvRepresentations
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,29 @@ object KeyReads extends EnvKeyReads with LowPriorityKeyReads {
}

private[json] sealed trait LowPriorityKeyReads {
implicit def readableKeyReads[T](implicit r: Reads[T]): KeyReads[T] =

/**
* Creates a [[KeyReads]] for values represented as JSON strings.
*
* The supplied [[Reads]] is applied to a [[JsString]] containing the object key.
* A [[Format.Representation]] is required to explicitly indicate that `T` is
* represented as a JSON string.
*
* @tparam T the type to read
* @return a [[KeyReads]] for `T`
*/
implicit def stringRepresentedKeyReads[T](implicit
r: Reads[T],
repr: Format.Representation[T, JsString]
): KeyReads[T] =
KeyReads[T] { key =>
r.reads(JsString(key))
}

@deprecated(
"Use stringRepresentedKeyReads instead. A Format.Representation[T, JsString] is required to ensure that T is represented as a JSON string.",
"3.1"
) def readableKeyReads[T](implicit r: Reads[T]): KeyReads[T] =
KeyReads[T] { key =>
r.reads(JsString(key))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trait KeyWrites[T] {
def writeKey(key: T): String
}

object KeyWrites extends EnvKeyWrites {
object KeyWrites extends EnvKeyWrites with LowPriorityKeyWrites {

/**
* Returns an instance which uses `f` as [[KeyWrites.writeKey]] function.
Expand All @@ -22,6 +22,32 @@ object KeyWrites extends EnvKeyWrites {
def writeKey(key: T) = f(key)
}

/**
* Creates a [[KeyWrites]] for values represented as JSON strings.
*
* The supplied [[Writes]] is expected to produce a [[JsString]] for every value.
* This method is unsafe in that an [[IllegalArgumentException]] is thrown if the
* [[Writes]] produces any other JSON value.
*
* @tparam T the type to write
* @return a [[KeyWrites]] for `T`
*/
implicit def stringRepresentedKeyWrites[T](implicit
w: Writes[T],
repr: Format.Representation[T, JsString]
): KeyWrites[T] = KeyWrites[T] {
w.writes(_) match {
case JsString(str) =>
str

case js =>
throw new IllegalArgumentException(s"${Json.prettyPrint(js)} is not represented as JSON string")
}
}
}

private[json] sealed trait LowPriorityKeyWrites {
implicit def anyValKeyWrites[T <: AnyVal]: KeyWrites[T] =
KeyWrites[T](_.toString)

}
13 changes: 8 additions & 5 deletions play-json/shared/src/main/scala/play/api/libs/json/Reads.scala
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ trait LowPriorityDefaultReads extends EnvReads {
ts.iterator.zipWithIndex
.foldLeft(JsSuccess {
val b = bf.newBuilder

b.sizeHint(ts)

b
}: JsResult[Builder[A, F[A]]]) { case (acc, (elem, idx)) =>
(acc, ra.reads(elem)) match {
Expand All @@ -297,9 +299,11 @@ trait LowPriorityDefaultReads extends EnvReads {
}
}
.map(_.result())

case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.jsarray"))))
}
}

}

/**
Expand Down Expand Up @@ -529,6 +533,7 @@ trait DefaultReads extends LowPriorityDefaultReads {
implicit def mapReads[K, V](k: String => JsResult[K])(implicit fmtv: Reads[V]): Reads[Map[K, V]] = Reads[Map[K, V]] {
case JsObject(m) => {
type Errors = Seq[(JsPath, Seq[JsonValidationError])]

def locate(e: Errors, key: String) = e.map { case (p, valerr) =>
(JsPath \ key) ++ p -> valerr
}
Expand Down Expand Up @@ -566,9 +571,8 @@ trait DefaultReads extends LowPriorityDefaultReads {
/**
* Deserializer for Array[T] types.
*/
implicit def ArrayReads[T: Reads: ClassTag]: Reads[Array[T]] = new Reads[Array[T]] {
def reads(json: JsValue) = json.validate[List[T]].map(_.toArray)
}
implicit def ArrayReads[T: Reads: ClassTag]: Reads[Array[T]] =
Reads[Array[T]](_.validate[List[T]].map(_.toArray))

/**
* Deserializer for java.net.URI
Expand All @@ -585,9 +589,8 @@ trait DefaultReads extends LowPriorityDefaultReads {
class UUIDReader(checkValidity: Boolean) extends Reads[java.util.UUID] {
import java.util.UUID

import scala.util.Try
def check(s: String)(u: UUID): Boolean = u != null && s == u.toString

def check(s: String)(u: UUID): Boolean = u != null && s == u.toString()
def parseUuid(s: String): Option[UUID] = {
val uncheckedUuid = Try(UUID.fromString(s)).toOption

Expand Down
Loading