Skip to content

Commit e55b765

Browse files
committed
accept miliseconds
1 parent 2999da8 commit e55b765

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

common/src/main/scala/com/gu/sfl/model/model.scala

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.gu.sfl.model
22

33
import java.io.IOException
4-
import java.time.format.DateTimeFormatter
4+
import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder}
5+
import java.time.temporal.ChronoField
56
import java.time.{Instant, LocalDateTime, ZoneOffset}
67

78
import com.fasterxml.jackson.annotation.JsonIgnore
@@ -80,7 +81,13 @@ case class ErrorResponse(status: String = "error", errors: List[Error])
8081
case class Error(message: String, description: String)
8182

8283
object SavedArticleDateSerializer {
83-
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
84+
/** Accept optional 3-digit milliseconds in input parsing since it is a valid ISO-8601 component
85+
* But drop them during output serialisation to ensure consistency in DB records and API responses
86+
*/
87+
val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]'Z'")
88+
val outputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
89+
90+
def parse(value: String): LocalDateTime = LocalDateTime.parse(value, inputFormatter)
8491
}
8592

8693
class DirtySavedArticleDeserializer(t: Class[DirtySavedArticle]) extends StdDeserializer[DirtySavedArticle](t) {
@@ -93,7 +100,7 @@ class DirtySavedArticleDeserializer(t: Class[DirtySavedArticle]) extends StdDese
93100
val id = Option(node.get("id")).filter(_.isTextual).map(_.asText())
94101
val shortUrl = Option(node.get("shortUrl")).filter(_.isTextual).map(_.asText())
95102
val read = Option(node.get("read")).filter(_.isBoolean).map(_.asBoolean())
96-
val date = Option(node.get("date")).filter(_.isTextual).map(_.asText()).map(LocalDateTime.parse(_, SavedArticleDateSerializer.formatter))
103+
val date = Option(node.get("date")).filter(_.isTextual).map(_.asText()).map(SavedArticleDateSerializer.parse)
97104
DirtySavedArticle(id, shortUrl, date, read.getOrElse(false))
98105
}
99106
}
@@ -109,7 +116,7 @@ class SavedArticleSerializer(t:Class[SavedArticle]) extends StdSerializer[SavedA
109116
gen.writeStartObject()
110117
gen.writeStringField("id", value.id)
111118
gen.writeStringField("shortUrl", value.shortUrl)
112-
gen.writeStringField("date", SavedArticleDateSerializer.formatter.format(value.date))
119+
gen.writeStringField("date", SavedArticleDateSerializer.outputFormatter.format(value.date))
113120
gen.writeBooleanField("read", value.read)
114121
gen.writeEndObject()
115122
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.gu.sfl.model
2+
3+
import java.time.LocalDateTime
4+
5+
import org.specs2.mutable.Specification
6+
7+
class SavedArticleDateSerializerTest extends Specification {
8+
"SavedArticleDateSerializer.parse" should {
9+
"parse a date without milliseconds" in {
10+
SavedArticleDateSerializer.parse("2026-07-17T10:15:30Z") must beEqualTo(
11+
LocalDateTime.of(2026, 7, 17, 10, 15, 30)
12+
)
13+
}
14+
15+
"parse a date with milliseconds" in {
16+
SavedArticleDateSerializer.parse("2026-07-17T10:15:30.123Z") must beEqualTo(
17+
LocalDateTime.of(2026, 7, 17, 10, 15, 30, 123000000)
18+
)
19+
}
20+
}
21+
22+
"SavedArticleDateSerializer.outputFormatter" should {
23+
"always write dates without milliseconds" in {
24+
val date = LocalDateTime.of(2026, 7, 17, 10, 15, 30, 123000000)
25+
SavedArticleDateSerializer.outputFormatter.format(date) must beEqualTo("2026-07-17T10:15:30Z")
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)