use ISO-8601 date/time format in Jackson serializer, #24155

* better for interoperability
* deserialization from both formats are supported
This commit is contained in:
Patrik Nordwall 2019-05-31 14:59:20 +02:00
parent cca13bdb32
commit 41600d3079
5 changed files with 138 additions and 19 deletions

View file

@ -4,6 +4,8 @@
package akka.serialization.jackson
import java.time.Instant
import java.time.LocalDateTime
import java.util.concurrent.TimeUnit
import scala.concurrent.Await
@ -28,6 +30,11 @@ object JacksonSerializationBench {
num1: Int,
num2: Int,
num3: Int,
flag1: Boolean,
flag2: Boolean,
duration: FiniteDuration,
date: LocalDateTime,
instant: Instant,
nested1: Small,
nested2: Small,
nested3: Small)
@ -41,6 +48,9 @@ object JacksonSerializationBench {
map: Map[String, Medium])
extends TestMessage
final class TimeMessage(val duration: FiniteDuration, val date: LocalDateTime, val instant: Instant)
extends TestMessage
// FIXME try with plain java classes (not case class)
}
@ -56,9 +66,51 @@ class JacksonSerializationBench {
val smallMsg1 = Small("abc", 17)
val smallMsg2 = Small("def", 18)
val smallMsg3 = Small("ghi", 19)
val mediumMsg1 = Medium("abc", "def", "ghi", 1, 2, 3, smallMsg1, smallMsg2, smallMsg3)
val mediumMsg2 = Medium("ABC", "DEF", "GHI", 10, 20, 30, smallMsg1, smallMsg2, smallMsg3)
val mediumMsg3 = Medium("abcABC", "defDEF", "ghiGHI", 100, 200, 300, smallMsg1, smallMsg2, smallMsg3)
val mediumMsg1 = Medium(
"abc",
"def",
"ghi",
1,
2,
3,
false,
true,
5.seconds,
LocalDateTime.of(2019, 4, 29, 23, 15, 3, 12345),
Instant.now(),
smallMsg1,
smallMsg2,
smallMsg3)
val mediumMsg2 = Medium(
"ABC",
"DEF",
"GHI",
10,
20,
30,
true,
false,
5.millis,
LocalDateTime.of(2019, 4, 29, 23, 15, 4, 12345),
Instant.now(),
smallMsg1,
smallMsg2,
smallMsg3)
val mediumMsg3 = Medium(
"abcABC",
"defDEF",
"ghiGHI",
100,
200,
300,
true,
true,
200.millis,
LocalDateTime.of(2019, 4, 29, 23, 15, 5, 12345),
Instant.now(),
smallMsg1,
smallMsg2,
smallMsg3)
val largeMsg = Large(
mediumMsg1,
mediumMsg2,
@ -66,10 +118,12 @@ class JacksonSerializationBench {
Vector(mediumMsg1, mediumMsg2, mediumMsg3),
Map("a" -> mediumMsg1, "b" -> mediumMsg2, "c" -> mediumMsg3))
val timeMsg = new TimeMessage(5.seconds, LocalDateTime.of(2019, 4, 29, 23, 15, 3, 12345), Instant.now())
var system: ActorSystem = _
var serialization: Serialization = _
@Param(Array("jackson-json", "jackson-smile", "jackson-cbor", "java"))
@Param(Array("jackson-json", "jackson-cbor", "jackson-smile")) // "java"
private var serializerName: String = _
@Setup(Level.Trial)
@ -83,7 +137,11 @@ class JacksonSerializationBench {
}
}
serialization.jackson {
#compress-larger-than = 100 b
compress-larger-than = 100000 b
serialization-features {
#WRITE_DATES_AS_TIMESTAMPS = off
}
}
}
""")
@ -127,4 +185,9 @@ class JacksonSerializationBench {
serializeDeserialize(largeMsg)
}
@Benchmark
def timeMessage(): TimeMessage = {
serializeDeserialize(timeMsg)
}
}

View file

@ -305,3 +305,21 @@ It's also possible to define several bindings and use different configuration fo
different settings for remote messages and persisted events.
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #several-config }
## Additional configuration
Additional Jackson serialization features can be enabled/disabled in configuration. The default values from
Jackson are used aside from the the following that are changed in Akka's default configuration.
@@snip [reference.conf](/akka-serialization-jackson/src/main/resources/reference.conf) { #features }
### Date/time format
`WRITE_DATES_AS_TIMESTAMPS` is by default disabled, which means that date/time fields are serialized in
ISO-8601 (rfc3339) `yyyy-MM-dd'T'HH:mm:ss.SSSZ` format instead of numeric arrays. This is better for
interoperability but it is slower. If you don't need the ISO format for interoperability with external systems
you can change the following configuration for better performance of date/time fields.
@@snip [config](/akka-serialization-jackson/src/test/scala/doc/akka/serialization/jackson/SerializationDocSpec.scala) { #date-time }
Jackson is still be able to deserialize the other format independent of this setting.

View file

@ -44,15 +44,23 @@ akka.serialization.jackson {
migrations {
}
}
#//#features
akka.serialization.jackson {
# Configuration of the ObjectMapper serialization features.
# See com.fasterxml.jackson.databind.SerializationFeature
# Enum values corresponding to the SerializationFeature and their boolean value.
serialization-features {
# Date/time in ISO-8601 (rfc3339) yyyy-MM-dd'T'HH:mm:ss.SSSZ format
# as defined by com.fasterxml.jackson.databind.util.StdDateFormat
# For interoperability it's better to use the ISO format, i.e. WRITE_DATES_AS_TIMESTAMPS=off,
# but WRITE_DATES_AS_TIMESTAMPS=on has better performance.
WRITE_DATES_AS_TIMESTAMPS = off
}
# Configuration of the ObjectMapper deserialization features.
# See com.fasterxml.jackson.databind.SeserializationFeature
# See com.fasterxml.jackson.databind.DeserializationFeature
# Enum values corresponding to the DeserializationFeature and their boolean value.
deserialization-features {
FAIL_ON_UNKNOWN_PROPERTIES = off
@ -71,6 +79,7 @@ akka.serialization.jackson {
jackson-smile {}
}
#//#features
akka.actor {
serializers {

View file

@ -158,32 +158,54 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
"JacksonJsonSerializer with Java message classes" must {
import JavaTestMessages._
// see SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
"by default serialize dates and durations as numeric timestamps" in {
// see SerializationFeature.WRITE_DATES_AS_TIMESTAMPS = off
"by default serialize dates and durations as text with ISO-8601 date format" in {
// Default format is defined in com.fasterxml.jackson.databind.util.StdDateFormat
// ISO-8601 yyyy-MM-dd'T'HH:mm:ss.SSSZ (rfc3339)
val msg = new TimeCommand(LocalDateTime.of(2019, 4, 29, 23, 15, 3, 12345), Duration.of(5, ChronoUnit.SECONDS))
val json = serializeToJsonString(msg)
val expected = """{"timestamp":[2019,4,29,23,15,3,12345],"duration":5.000000000}"""
val expected = """{"timestamp":"2019-04-29T23:15:03.000012345","duration":"PT5S"}"""
json should ===(expected)
// and full round trip
checkSerialization(msg)
// and it can still deserialize from numeric timestamps format
val serializer = serializerFor(msg)
val manifest = serializer.manifest(msg)
val serializerId = serializer.identifier
val deserializedFromTimestampsFormat = deserializeFromJsonString(
"""{"timestamp":[2019,4,29,23,15,3,12345],"duration":5.000000000}""",
serializerId,
manifest)
deserializedFromTimestampsFormat should ===(msg)
}
// see SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
"be possible to serialize dates and durations as text with default date format " in {
// see SerializationFeature.WRITE_DATES_AS_TIMESTAMPS = on
"be possible to serialize dates and durations as numeric timestamps" in {
withSystem("""
akka.serialization.jackson.serialization-features {
WRITE_DATES_AS_TIMESTAMPS = off
WRITE_DATES_AS_TIMESTAMPS = on
}
""") { sys =>
val msg = new TimeCommand(LocalDateTime.of(2019, 4, 29, 23, 15, 3, 12345), Duration.of(5, ChronoUnit.SECONDS))
val json = serializeToJsonString(msg, sys)
// Default format is defined in com.fasterxml.jackson.databind.util.StdDateFormat
// ISO-8601 yyyy-MM-dd'T'HH:mm:ss.SSSZ
// FIXME is this the same as rfc3339, or do we need something else to support interop with the format used by Play JSON?
// FIXME should we make this the default rather than numberic timestamps?
val expected = """{"timestamp":"2019-04-29T23:15:03.000012345","duration":"PT5S"}"""
val expected = """{"timestamp":[2019,4,29,23,15,3,12345],"duration":5.000000000}"""
json should ===(expected)
// and full round trip
checkSerialization(msg)
checkSerialization(msg, sys)
// and it can still deserialize from ISO format
val serializer = serializerFor(msg, sys)
val manifest = serializer.manifest(msg)
val serializerId = serializer.identifier
val deserializedFromIsoFormat = deserializeFromJsonString(
"""{"timestamp":"2019-04-29T23:15:03.000012345","duration":"PT5S"}""",
serializerId,
manifest,
sys)
deserializedFromIsoFormat should ===(msg)
}
}

View file

@ -104,5 +104,12 @@ object SerializationDocSpec {
final case class Elephant(name: String, age: Int) extends Animal
//#polymorphism
val configDateTime = """
#//#date-time
akka.serialization.jackson.serialization-features {
WRITE_DATES_AS_TIMESTAMPS = on
}
#//#date-time
"""
}
// FIXME add real tests for the migrations, see EventMigrationTest.java in Lagom