diff --git a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala b/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala index 7216c383..9fd0afd9 100644 --- a/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala +++ b/json/json-core/src/main/scala/io/sphere/json/FromJSON.scala @@ -380,8 +380,32 @@ object FromJSON extends FromJSONInstances { } // java.time + // this formatter is used to parse instant in an extra lenient way + // similar to what the joda `DateTime` constructor accepts + // the accepted grammar for joda is described here: https://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTimeParser-- + // this only supports the part where the date is specified + private val lenientInstantParser = + new time.format.DateTimeFormatterBuilder() + .appendPattern("uuuu[-MM[-dd]]") + .optionalStart() + .appendPattern("'T'[HH[:mm[:ss]]]") + .appendFraction(time.temporal.ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalStart() + .appendZoneOrOffsetId() + .optionalEnd() + .optionalEnd() + .parseDefaulting(time.temporal.ChronoField.MONTH_OF_YEAR, 1L) + .parseDefaulting(time.temporal.ChronoField.DAY_OF_MONTH, 1L) + .parseDefaulting(time.temporal.ChronoField.HOUR_OF_DAY, 0L) + .parseDefaulting(time.temporal.ChronoField.MINUTE_OF_HOUR, 0L) + .parseDefaulting(time.temporal.ChronoField.SECOND_OF_MINUTE, 0L) + .parseDefaulting(time.temporal.ChronoField.NANO_OF_SECOND, 0L) + .toFormatter() + .withZone(time.ZoneOffset.UTC) + implicit val javaInstantReader: FromJSON[time.Instant] = - jsonStringReader("Failed to parse date/time: %s")(time.Instant.parse(_)) + jsonStringReader("Failed to parse date/time: %s")(s => + time.Instant.from(lenientInstantParser.parse(s))) implicit val javaLocalTimeReader: FromJSON[time.LocalTime] = jsonStringReader("Failed to parse time: %s")( diff --git a/json/json-core/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala b/json/json-core/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala index 71a6e8b5..aec11c62 100644 --- a/json/json-core/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala +++ b/json/json-core/src/test/scala/io/sphere/json/DateTimeParsingSpec.scala @@ -3,6 +3,8 @@ package io.sphere.json import org.json4s.JString import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec +import java.time.Instant +import cats.data.Validated.Valid class DateTimeParsingSpec extends AnyWordSpec with Matchers { @@ -107,4 +109,65 @@ class DateTimeParsingSpec extends AnyWordSpec with Matchers { javaInstantReader.read(jsonDateStringWith(secondOfTheMinute = "87")) shouldNot beValid } } + + // ported from https://github.com/JodaOrg/joda-time/blob/4a1402a47cab4636bf4c73d42a62bfa80c1535ca/src/test/java/org/joda/time/convert/TestStringConverter.java#L114-L156 + // ensures that we accept similar patterns as joda when parsing instants + "parsing a Java instant" should { + "accept a full instant with milliseconds and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48.501+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:48.501Z")) + } + + "accept a year with offset" in { + javaInstantReader.read(JString("2004T+08:00")) shouldBe Valid( + Instant.parse("2004-01-01T00:00:00+08:00")) + } + + "accept a year month with offset" in { + javaInstantReader.read(JString("2004-06T+08:00")) shouldBe Valid( + Instant.parse("2004-06-01T00:00:00+08:00")) + } + + "accept a year month day with offset" in { + javaInstantReader.read(JString("2004-06-09T+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T00:00:00+08:00")) + } + + "accept a year month day with hour and offset" in { + javaInstantReader.read(JString("2004-06-09T12+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:00:00Z")) + } + + "accept a year month day with hour, minute, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:00Z")) + } + + "accept a year month day with hour, minute, second, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:48Z")) + } + + "accept a year month day with hour, fraction, and offset" in { + javaInstantReader.read(JString("2004-06-09T12.5+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:00:00.5Z")) + } + + "accept a year month day with hour, minute, fraction, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24.5+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:00.5Z")) + } + + "accept a year month day with hour, minute, second, fraction, and offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48.5+08:00")) shouldBe Valid( + Instant.parse("2004-06-09T04:24:48.5Z")) + } + + "accept a year month day with hour, minute, second, fraction, but no offset" in { + javaInstantReader.read(JString("2004-06-09T12:24:48.501")) shouldBe Valid( + Instant.parse("2004-06-09T12:24:48.501Z")) + } + + } + }