Skip to content

Commit

Permalink
More efficient decoding of UUID, Currency, and java.time._ valu…
Browse files Browse the repository at this point in the history
…es (#1231)
  • Loading branch information
plokhotnyuk authored Jan 20, 2025
1 parent 953f7ed commit 774ebd6
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 45 deletions.
106 changes: 65 additions & 41 deletions zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -445,14 +445,14 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with
new JsonDecoder[A] {
def unsafeDecode(trace: List[JsonError], in: RetractReader): A =
f(string.unsafeDecode(trace, in)) match {
case Left(err) => Lexer.error(err, trace)
case Right(value) => value
case Left(err) => Lexer.error(err, trace)
}

override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A =
f(string.unsafeFromJsonAST(trace, json)) match {
case Left(err) => Lexer.error(err, trace)
case Right(value) => value
case Left(err) => Lexer.error(err, trace)
}
}
}
Expand Down Expand Up @@ -691,31 +691,42 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 {
import java.time.format.DateTimeParseException
import java.time.zone.ZoneRulesException

implicit val dayOfWeek: JsonDecoder[DayOfWeek] = mapStringOrFail(s => parseJavaTime(DayOfWeek.valueOf, s.toUpperCase))
implicit val duration: JsonDecoder[Duration] = mapStringOrFail(parseJavaTime(parsers.unsafeParseDuration, _))
implicit val instant: JsonDecoder[Instant] = mapStringOrFail(parseJavaTime(parsers.unsafeParseInstant, _))
implicit val localDate: JsonDecoder[LocalDate] = mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalDate, _))

implicit val localDateTime: JsonDecoder[LocalDateTime] =
mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalDateTime, _))

implicit val localTime: JsonDecoder[LocalTime] = mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalTime, _))
implicit val month: JsonDecoder[Month] = mapStringOrFail(s => parseJavaTime(Month.valueOf, s.toUpperCase))
implicit val monthDay: JsonDecoder[MonthDay] = mapStringOrFail(parseJavaTime(parsers.unsafeParseMonthDay, _))

implicit val offsetDateTime: JsonDecoder[OffsetDateTime] =
mapStringOrFail(parseJavaTime(parsers.unsafeParseOffsetDateTime, _))

implicit val offsetTime: JsonDecoder[OffsetTime] = mapStringOrFail(parseJavaTime(parsers.unsafeParseOffsetTime, _))
implicit val period: JsonDecoder[Period] = mapStringOrFail(parseJavaTime(parsers.unsafeParsePeriod, _))
implicit val year: JsonDecoder[Year] = mapStringOrFail(parseJavaTime(parsers.unsafeParseYear, _))
implicit val yearMonth: JsonDecoder[YearMonth] = mapStringOrFail(parseJavaTime(parsers.unsafeParseYearMonth, _))

implicit val zonedDateTime: JsonDecoder[ZonedDateTime] =
mapStringOrFail(parseJavaTime(parsers.unsafeParseZonedDateTime, _))

implicit val zoneId: JsonDecoder[ZoneId] = mapStringOrFail(parseJavaTime(parsers.unsafeParseZoneId, _))
implicit val zoneOffset: JsonDecoder[ZoneOffset] = mapStringOrFail(parseJavaTime(parsers.unsafeParseZoneOffset, _))
implicit val dayOfWeek: JsonDecoder[DayOfWeek] = javaTimeDecoder(s => DayOfWeek.valueOf(s.toUpperCase))
implicit val duration: JsonDecoder[Duration] = javaTimeDecoder(parsers.unsafeParseDuration)
implicit val instant: JsonDecoder[Instant] = javaTimeDecoder(parsers.unsafeParseInstant)
implicit val localDate: JsonDecoder[LocalDate] = javaTimeDecoder(parsers.unsafeParseLocalDate)
implicit val localDateTime: JsonDecoder[LocalDateTime] = javaTimeDecoder(parsers.unsafeParseLocalDateTime)
implicit val localTime: JsonDecoder[LocalTime] = javaTimeDecoder(parsers.unsafeParseLocalTime)
implicit val month: JsonDecoder[Month] = javaTimeDecoder(s => Month.valueOf(s.toUpperCase))
implicit val monthDay: JsonDecoder[MonthDay] = javaTimeDecoder(parsers.unsafeParseMonthDay)
implicit val offsetDateTime: JsonDecoder[OffsetDateTime] = javaTimeDecoder(parsers.unsafeParseOffsetDateTime)
implicit val offsetTime: JsonDecoder[OffsetTime] = javaTimeDecoder(parsers.unsafeParseOffsetTime)
implicit val period: JsonDecoder[Period] = javaTimeDecoder(parsers.unsafeParsePeriod)
implicit val year: JsonDecoder[Year] = javaTimeDecoder(parsers.unsafeParseYear)
implicit val yearMonth: JsonDecoder[YearMonth] = javaTimeDecoder(parsers.unsafeParseYearMonth)
implicit val zonedDateTime: JsonDecoder[ZonedDateTime] = javaTimeDecoder(parsers.unsafeParseZonedDateTime)
implicit val zoneId: JsonDecoder[ZoneId] = javaTimeDecoder(parsers.unsafeParseZoneId)
implicit val zoneOffset: JsonDecoder[ZoneOffset] = javaTimeDecoder(parsers.unsafeParseZoneOffset)

private[this] def javaTimeDecoder[A](f: String => A): JsonDecoder[A] = new JsonDecoder[A] {
def unsafeDecode(trace: List[JsonError], in: RetractReader): A =
parseJavaTime(trace, string.unsafeDecode(trace, in))

override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A =
parseJavaTime(trace, string.unsafeFromJsonAST(trace, json))

// Commonized handling for decoding from string to java.time Class
@inline
private[this] def parseJavaTime(trace: List[JsonError], s: String): A =
try f(s)
catch {
case zre: ZoneRulesException => Lexer.error(s"$s is not a valid ISO-8601 format, ${zre.getMessage}", trace)
case dtpe: DateTimeParseException =>
Lexer.error(s"$s is not a valid ISO-8601 format, ${dtpe.getMessage}", trace)
case dte: DateTimeException => Lexer.error(s"$s is not a valid ISO-8601 format, ${dte.getMessage}", trace)
case ex: Exception => Lexer.error(ex.getMessage, trace)
}
}

// Commonized handling for decoding from string to java.time Class
private[json] def parseJavaTime[A](f: String => A, s: String): Either[String, A] =
Expand All @@ -728,25 +739,38 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 {
case ex: Exception => Left(ex.getMessage)
}

implicit val uuid: JsonDecoder[UUID] =
mapStringOrFail { str =>
try {
Right(UUIDParser.unsafeParse(str))
} catch {
case iae: IllegalArgumentException => Left(s"Invalid UUID: ${iae.getMessage}")
implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] {
def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID =
parseUUID(trace, string.unsafeDecode(trace, in))

override def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID =
parseUUID(trace, string.unsafeFromJsonAST(trace, json))

@inline
private[this] def parseUUID(trace: List[JsonError], s: String): UUID =
try UUIDParser.unsafeParse(s)
catch {
case iae: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${iae.getMessage}", trace)
}
}
}

implicit val currency: JsonDecoder[java.util.Currency] = new JsonDecoder[java.util.Currency] {
def unsafeDecode(trace: List[JsonError], in: RetractReader): java.util.Currency =
parseCurrency(trace, string.unsafeDecode(trace, in))

override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency =
parseCurrency(trace, string.unsafeFromJsonAST(trace, json))

implicit val currency: JsonDecoder[java.util.Currency] =
mapStringOrFail { str =>
try {
Right(java.util.Currency.getInstance(str))
} catch {
case iae: IllegalArgumentException => Left(s"Invalid Currency: ${iae.getMessage}")
@inline
private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency =
try java.util.Currency.getInstance(s)
catch {
case iae: IllegalArgumentException => Lexer.error(s"Invalid Currency: ${iae.getMessage}", trace)
}
}
}
}

private[json] trait DecoderLowPriority4 extends DecoderLowPriorityVersionSpecific {
@inline
implicit def fromCodec[A](implicit codec: JsonCodec[A]): JsonDecoder[A] = codec.decoder
}
Original file line number Diff line number Diff line change
Expand Up @@ -1572,6 +1572,7 @@ private[json] object parsers {

private[this] def charError(ch: Char, pos: Int) = error(s"expected '$ch'", pos)

private[this] def error(msg: String, pos: Int) =
@noinline
private[this] def error(msg: String, pos: Int): Nothing =
throw new DateTimeException(msg + " at index " + pos) with NoStackTrace
}
8 changes: 5 additions & 3 deletions zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package zio.json.uuid

import scala.annotation.nowarn
import scala.util.control.NoStackTrace

// A port of https://github.com/openjdk/jdk/commit/ebadfaeb2e1cc7b5ce5f101cd8a539bc5478cf5b with optimizations applied
private[json] object UUIDParser {
Expand Down Expand Up @@ -89,7 +90,7 @@ private[json] object UUIDParser {

private[this] def unsafeParseExtended(input: String): java.util.UUID = {
val len = input.length
if (len > 36) throw new IllegalArgumentException("UUID string too large")
if (len > 36) invalidUUIDError("UUID string too large")
val dash1 = input.indexOf('-', 0)
val dash2 = input.indexOf('-', dash1 + 1)
val dash3 = input.indexOf('-', dash2 + 1)
Expand Down Expand Up @@ -131,6 +132,7 @@ private[json] object UUIDParser {
result
}

private[this] def invalidUUIDError(input: String): IllegalArgumentException =
throw new IllegalArgumentException(input)
@noinline
private[this] def invalidUUIDError(input: String): Nothing =
throw new IllegalArgumentException(input) with NoStackTrace
}

0 comments on commit 774ebd6

Please sign in to comment.