Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[finatra-jackson] Add c.t.u.Time deserializer with JsonFormat support
**Problem** Currently `FinatraJacksonModule` doesn't support deserializing `com.twitter.util.Time` **Solution** Added `TimeStringDeserializer` class which adds the functionality to deserialize a time string (e.g. `2019-06-19T15:27:00.000-07:00`) into a `com.twitter.util.Time` object. It also supports Jackson's `JsonFormat` annotation to specify the pattern of the input string (e.g `yyyy-MM-dd`). The pattern follows the `java.text.SimpleDateFormat` style. Defaults the timezone to `UTC` if not specified. **Result** Time strings can be successfully deserialized into `com.twitter.util.Time` objects. Differential Revision: https://phabricator.twitter.biz/D330682
- Loading branch information
Rahul Iyer
authored and
jenkins
committed
Jun 21, 2019
1 parent
ea0b7a6
commit ed3d666
Showing
7 changed files
with
169 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
...er/finatra/http/tests/integration/doeverything/main/domain/DomainTwitterTimeRequest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.twitter.finatra.http.tests.integration.doeverything.main.domain | ||
|
||
import com.twitter.util.Time | ||
|
||
case class DomainTwitterTimeRequest(time: Time) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
jackson/src/main/scala/com/twitter/finatra/json/internal/serde/TimeStringDeserializer.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package com.twitter.finatra.json.internal.serde | ||
|
||
import com.fasterxml.jackson.core.JsonParser | ||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer | ||
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer | ||
import com.fasterxml.jackson.databind.{BeanProperty, DeserializationContext, JsonDeserializer} | ||
import com.twitter.util.{Time, TimeFormat} | ||
import java.util.{Locale, TimeZone} | ||
|
||
private[finatra] object TimeStringDeserializer { | ||
private[this] val DefaultTimeFormat: String = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" | ||
|
||
def apply(): TimeStringDeserializer = this(DefaultTimeFormat) | ||
|
||
def apply(pattern: String): TimeStringDeserializer = | ||
this(pattern, None, TimeZone.getTimeZone("UTC")) | ||
|
||
def apply(pattern: String, locale: Option[Locale], timezone: TimeZone): TimeStringDeserializer = | ||
new TimeStringDeserializer(new TimeFormat(pattern, locale, timezone)) | ||
} | ||
|
||
private[finatra] class TimeStringDeserializer( | ||
private[this] val timeFormat: TimeFormat | ||
) extends StdScalarDeserializer[Time](classOf[Time]) with ContextualDeserializer { | ||
|
||
override def deserialize(parser: JsonParser, context: DeserializationContext): Time = { | ||
timeFormat.parse(parser.getValueAsString) | ||
} | ||
|
||
/** | ||
* This method allows extracting the [[com.fasterxml.jackson.annotation.JsonFormat JsonFormat]] | ||
* annotation and create a [[com.twitter.util.TimeFormat TimeFormat]] based on the specifications | ||
* provided in the annotation. The implementation follows the Jackson's java8 & joda-time versions | ||
* | ||
* @param context Deserialization context to access configuration, additional deserializers | ||
* that may be needed by this deserializer | ||
* @param property Method, field or constructor parameter that represents the property (and is | ||
* used to assign deserialized value). Should be available; but there may be | ||
* cases where caller can not provide it and null is passed instead | ||
* (in which case impls usually pass 'this' deserializer as is) | ||
* | ||
* @return Deserializer to use for deserializing values of specified property; may be this | ||
* instance or a new instance. | ||
* | ||
* @see https://github.com/FasterXML/jackson-modules-java8/blob/master/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DateTimeDeserializerBase.java#L29 | ||
*/ | ||
override def createContextual( | ||
context: DeserializationContext, | ||
property: BeanProperty | ||
): JsonDeserializer[_] = { | ||
val deserializerOption: Option[TimeStringDeserializer] = for { | ||
jsonFormat <- Option(findFormatOverrides(context, property, handledType())) | ||
deserializer <- Option(TimeStringDeserializer( | ||
jsonFormat.getPattern, | ||
Option(jsonFormat.getLocale), | ||
Option(jsonFormat.getTimeZone).getOrElse(TimeZone.getTimeZone("UTC")) | ||
)) if jsonFormat.hasPattern | ||
} yield { | ||
deserializer | ||
} | ||
deserializerOption.getOrElse(this) | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
...est/scala/com/twitter/finatra/json/tests/internal/TwitterTimeStringDeserializerTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.twitter.finatra.json.tests.internal | ||
|
||
import com.fasterxml.jackson.annotation.JsonFormat | ||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.fasterxml.jackson.module.scala.DefaultScalaModule | ||
import com.twitter.finatra.json.internal.serde.SerDeSimpleModule | ||
import com.twitter.inject.Test | ||
import com.twitter.util.{Time, TimeFormat} | ||
|
||
case class WithoutJsonFormat(time: Time) | ||
|
||
case class WithJsonFormat(@JsonFormat(pattern = "yyyy-MM-dd") time: Time) | ||
|
||
case class WithJsonFormatAndTimezone( | ||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "America/Los_Angeles") time: Time | ||
) | ||
|
||
class TwitterTimeStringDeserializerTest extends Test { | ||
|
||
private[this] final val Input1 = | ||
""" | ||
|{ | ||
| "time": "2019-06-17T15:45:00.000+0000" | ||
|} | ||
""".stripMargin | ||
|
||
private[this] final val Input2 = | ||
""" | ||
|{ | ||
| "time": "2019-06-17" | ||
|} | ||
""".stripMargin | ||
|
||
private[this] final val Input3 = | ||
""" | ||
|{ | ||
| "time": "2019-06-17 16:30:00" | ||
|} | ||
""".stripMargin | ||
|
||
private[this] val objectMapper = new ObjectMapper() | ||
|
||
override def beforeAll(): Unit = { | ||
val modules = Seq(DefaultScalaModule, SerDeSimpleModule) | ||
modules.foreach(objectMapper.registerModule) | ||
} | ||
|
||
test("should deserialize date without JsonFormat") { | ||
val expected = 1560786300000L | ||
val actual: WithoutJsonFormat = objectMapper.readValue(Input1, classOf[WithoutJsonFormat]) | ||
actual.time.inMillis shouldEqual expected | ||
} | ||
|
||
test("should deserialize date with JsonFormat") { | ||
val expected: Time = new TimeFormat("yyyy-MM-dd").parse("2019-06-17") | ||
val actual: WithJsonFormat = objectMapper.readValue(Input2, classOf[WithJsonFormat]) | ||
actual.time shouldEqual expected | ||
} | ||
|
||
test("should deserialize date with JsonFormat and timezone") { | ||
val expected = 1560814200000L | ||
val actual: WithJsonFormatAndTimezone = objectMapper.readValue( | ||
Input3, classOf[WithJsonFormatAndTimezone]) | ||
actual.time.inMillis shouldEqual expected | ||
} | ||
|
||
} |