Add configuration for all Jackson Features (#27409)
* Support configuration for Jackson MapperFeatures in Jackson Serializer * Add JsonParser.Feature configuration support * Add JsonGenerator.Feature configuration support * Fix formatting issues * Add examples for each feature configuration * Test coverage of the override methods
This commit is contained in:
parent
16f4971f64
commit
9caae087a2
3 changed files with 212 additions and 17 deletions
|
|
@ -64,6 +64,36 @@ akka.serialization.jackson {
|
|||
FAIL_ON_UNKNOWN_PROPERTIES = off
|
||||
}
|
||||
|
||||
# Configuration of the ObjectMapper mapper features.
|
||||
# See com.fasterxml.jackson.databind.MapperFeature
|
||||
# Enum values corresponding to the MapperFeature and their
|
||||
# boolean values, for example:
|
||||
#
|
||||
# mapper-features {
|
||||
# SORT_PROPERTIES_ALPHABETICALLY = on
|
||||
# }
|
||||
mapper-features {}
|
||||
|
||||
# Configuration of the ObjectMapper JsonParser features.
|
||||
# See com.fasterxml.jackson.core.JsonParser.Feature
|
||||
# Enum values corresponding to the JsonParser.Feature and their
|
||||
# boolean value, for example:
|
||||
#
|
||||
# json-parser-features {
|
||||
# ALLOW_SINGLE_QUOTES = on
|
||||
# }
|
||||
json-parser-features {}
|
||||
|
||||
# Configuration of the ObjectMapper JsonParser features.
|
||||
# See com.fasterxml.jackson.core.JsonGenerator.Feature
|
||||
# Enum values corresponding to the JsonGenerator.Feature and
|
||||
# their boolean value, for example:
|
||||
#
|
||||
# json-generator-features {
|
||||
# WRITE_NUMBERS_AS_STRINGS = on
|
||||
# }
|
||||
json-generator-features {}
|
||||
|
||||
# Additional classes that are allowed even if they are not defined in `serialization-bindings`.
|
||||
# This is useful when a class is not used for serialization any more and therefore removed
|
||||
# from `serialization-bindings`, but should still be possible to deserialize.
|
||||
|
|
|
|||
|
|
@ -28,11 +28,14 @@ import com.fasterxml.jackson.annotation.JsonCreator
|
|||
import com.fasterxml.jackson.annotation.PropertyAccessor
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.MapperFeature
|
||||
import com.fasterxml.jackson.databind.Module
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
|
||||
import com.typesafe.config.Config
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
|
||||
object JacksonObjectMapperProvider extends ExtensionId[JacksonObjectMapperProvider] with ExtensionIdProvider {
|
||||
override def get(system: ActorSystem): JacksonObjectMapperProvider = super.get(system)
|
||||
|
|
@ -94,6 +97,32 @@ object JacksonObjectMapperProvider extends ExtensionId[JacksonObjectMapperProvid
|
|||
case (feature, value) => mapper.configure(feature, value)
|
||||
}
|
||||
|
||||
val configuredMapperFeatures = features(config, "mapper-features").map {
|
||||
case (enumName, value) => MapperFeature.valueOf(enumName) -> value
|
||||
}
|
||||
val mapperFeatures = objectMapperFactory.overrideConfiguredMapperFeatures(bindingName, configuredMapperFeatures)
|
||||
mapperFeatures.foreach {
|
||||
case (feature, value) => mapper.configure(feature, value)
|
||||
}
|
||||
|
||||
val configuredJsonParserFeatures = features(config, "json-parser-features").map {
|
||||
case (enumName, value) => JsonParser.Feature.valueOf(enumName) -> value
|
||||
}
|
||||
val jsonParserFeatures =
|
||||
objectMapperFactory.overrideConfiguredJsonParserFeatures(bindingName, configuredJsonParserFeatures)
|
||||
jsonParserFeatures.foreach {
|
||||
case (feature, value) => mapper.configure(feature, value)
|
||||
}
|
||||
|
||||
val configuredJsonGeneratorFeatures = features(config, "json-generator-features").map {
|
||||
case (enumName, value) => JsonGenerator.Feature.valueOf(enumName) -> value
|
||||
}
|
||||
val jsonGeneratorFeatures =
|
||||
objectMapperFactory.overrideConfiguredJsonGeneratorFeatures(bindingName, configuredJsonGeneratorFeatures)
|
||||
jsonGeneratorFeatures.foreach {
|
||||
case (feature, value) => mapper.configure(feature, value)
|
||||
}
|
||||
|
||||
val configuredModules = config.getStringList("jackson-modules").asScala
|
||||
val modules1 =
|
||||
configuredModules.flatMap { fqcn =>
|
||||
|
|
@ -327,4 +356,43 @@ class JacksonObjectMapperFactory {
|
|||
configuredModules: immutable.Seq[Module]): immutable.Seq[Module] =
|
||||
configuredModules
|
||||
|
||||
/**
|
||||
* After construction of the `ObjectMapper` the configured mapper features are applied to
|
||||
* the mapper. These features can be amended programmatically by overriding this method and
|
||||
* return the features that are to be applied to the `ObjectMapper`.
|
||||
*
|
||||
* @param bindingName bindingName name of this `ObjectMapper`
|
||||
* @param configuredFeatures the list of `MapperFeatures` that were configured in `akka.serialization.jackson.mapper-features`
|
||||
*/
|
||||
def overrideConfiguredMapperFeatures(
|
||||
@unused bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(MapperFeature, Boolean)]): immutable.Seq[(MapperFeature, Boolean)] =
|
||||
configuredFeatures
|
||||
|
||||
/**
|
||||
* After construction of the `ObjectMapper` the configured JSON parser features are applied to
|
||||
* the mapper. These features can be amended programmatically by overriding this method and
|
||||
* return the features that are to be applied to the `ObjectMapper`.
|
||||
*
|
||||
* @param bindingName bindingName name of this `ObjectMapper`
|
||||
* @param configuredFeatures the list of `JsonParser.Feature` that were configured in `akka.serialization.jackson.json-parser-features`
|
||||
*/
|
||||
def overrideConfiguredJsonParserFeatures(
|
||||
@unused bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(JsonParser.Feature, Boolean)]): immutable.Seq[(JsonParser.Feature, Boolean)] =
|
||||
configuredFeatures
|
||||
|
||||
/**
|
||||
* After construction of the `ObjectMapper` the configured JSON generator features are applied to
|
||||
* the mapper. These features can be amended programmatically by overriding this method and
|
||||
* return the features that are to be applied to the `ObjectMapper`.
|
||||
*
|
||||
* @param bindingName bindingName name of this `ObjectMapper`
|
||||
* @param configuredFeatures the list of `JsonGenerator.Feature` that were configured in `akka.serialization.jackson.json-generator-features`
|
||||
*/
|
||||
def overrideConfiguredJsonGeneratorFeatures(
|
||||
@unused bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(JsonGenerator.Feature, Boolean)])
|
||||
: immutable.Seq[(JsonGenerator.Feature, Boolean)] =
|
||||
configuredFeatures
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import akka.testkit.TestKit
|
|||
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||
import com.fasterxml.jackson.core.JsonFactory
|
||||
import com.fasterxml.jackson.databind.MapperFeature
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.Module
|
||||
|
|
@ -45,6 +46,8 @@ import com.typesafe.config.ConfigFactory
|
|||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.Matchers
|
||||
import org.scalatest.WordSpecLike
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
|
||||
object ScalaTestMessages {
|
||||
trait TestMessage
|
||||
|
|
@ -137,23 +140,84 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
|
|||
JacksonObjectMapperProvider(system).getOrCreate(anotherBindingName, None) shouldBe theSameInstanceAs(mapper2)
|
||||
}
|
||||
|
||||
"support several different configurations" in {
|
||||
"JacksonSerializer configuration" must {
|
||||
|
||||
withSystem("""
|
||||
akka.actor.serializers.jackson-json2 = "akka.serialization.jackson.JacksonJsonSerializer"
|
||||
akka.actor.serialization-identifiers.jackson-json2 = 999
|
||||
akka.serialization.jackson.jackson-json2 {
|
||||
deserialization-features.FAIL_ON_UNKNOWN_PROPERTIES = on
|
||||
}
|
||||
""") { sys =>
|
||||
val objMapper2 = serialization(sys).serializerByIdentity(999).asInstanceOf[JacksonJsonSerializer].objectMapper
|
||||
objMapper2.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) should ===(true)
|
||||
val objMapper3 = JacksonObjectMapperProvider(sys).getOrCreate("jackson-json2", None)
|
||||
objMapper3.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) should ===(true)
|
||||
|
||||
// default has different config, different instance but same JacksonJsonSerializer class
|
||||
val objMapper =
|
||||
# on is Jackson's default
|
||||
serialization-features.WRITE_DURATIONS_AS_TIMESTAMPS = off
|
||||
|
||||
# on is Jackson's default
|
||||
deserialization-features.EAGER_DESERIALIZER_FETCH = off
|
||||
|
||||
# off is Jackson's default
|
||||
mapper-features.SORT_PROPERTIES_ALPHABETICALLY = on
|
||||
|
||||
# off is Jackson's default
|
||||
json-parser-features.ALLOW_COMMENTS = on
|
||||
|
||||
# on is Jackson's default
|
||||
json-generator-features.AUTO_CLOSE_TARGET = off
|
||||
}
|
||||
""") { sys =>
|
||||
val identifiedObjectMapper =
|
||||
serialization(sys).serializerByIdentity(999).asInstanceOf[JacksonJsonSerializer].objectMapper
|
||||
val namedObjectMapper = JacksonObjectMapperProvider(sys).getOrCreate("jackson-json2", None)
|
||||
val defaultObjectMapper =
|
||||
serializerFor(ScalaTestMessages.SimpleCommand("abc")).asInstanceOf[JacksonJsonSerializer].objectMapper
|
||||
objMapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) should ===(false)
|
||||
|
||||
"support serialization features" in {
|
||||
identifiedObjectMapper.isEnabled(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) should ===(false)
|
||||
namedObjectMapper.isEnabled(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) should ===(false)
|
||||
|
||||
// Default mapper follows Jackson and reference.conf default configuration
|
||||
defaultObjectMapper.isEnabled(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) should ===(true)
|
||||
}
|
||||
|
||||
"support deserialization features" in {
|
||||
identifiedObjectMapper.isEnabled(DeserializationFeature.EAGER_DESERIALIZER_FETCH) should ===(false)
|
||||
namedObjectMapper.isEnabled(DeserializationFeature.EAGER_DESERIALIZER_FETCH) should ===(false)
|
||||
|
||||
// Default mapper follows Jackson and reference.conf default configuration
|
||||
defaultObjectMapper.isEnabled(DeserializationFeature.EAGER_DESERIALIZER_FETCH) should ===(true)
|
||||
}
|
||||
|
||||
"support mapper features" in {
|
||||
identifiedObjectMapper.isEnabled(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) should ===(true)
|
||||
namedObjectMapper.isEnabled(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) should ===(true)
|
||||
|
||||
// Default mapper follows Jackson and reference.conf default configuration
|
||||
defaultObjectMapper.isEnabled(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) should ===(false)
|
||||
}
|
||||
|
||||
"support json parser features" in {
|
||||
identifiedObjectMapper.isEnabled(JsonParser.Feature.ALLOW_COMMENTS) should ===(true)
|
||||
namedObjectMapper.isEnabled(JsonParser.Feature.ALLOW_COMMENTS) should ===(true)
|
||||
|
||||
// Default mapper follows Jackson and reference.conf default configuration
|
||||
defaultObjectMapper.isEnabled(JsonParser.Feature.ALLOW_COMMENTS) should ===(false)
|
||||
}
|
||||
|
||||
"support json generator features" in {
|
||||
identifiedObjectMapper.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET) should ===(false)
|
||||
namedObjectMapper.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET) should ===(false)
|
||||
|
||||
// Default mapper follows Jackson and reference.conf default configuration
|
||||
defaultObjectMapper.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET) should ===(true)
|
||||
}
|
||||
|
||||
"fallback to defaults when object mapper is not configured" in {
|
||||
val notConfigured = JacksonObjectMapperProvider(sys).getOrCreate("jackson-not-configured", None)
|
||||
// Use Jacksons and Akka defaults
|
||||
notConfigured.isEnabled(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) should ===(true)
|
||||
notConfigured.isEnabled(DeserializationFeature.EAGER_DESERIALIZER_FETCH) should ===(true)
|
||||
notConfigured.isEnabled(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) should ===(false)
|
||||
notConfigured.isEnabled(JsonParser.Feature.ALLOW_COMMENTS) should ===(false)
|
||||
notConfigured.isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET) should ===(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,19 +318,45 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
|
|||
bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(SerializationFeature, Boolean)])
|
||||
: immutable.Seq[(SerializationFeature, Boolean)] = {
|
||||
if (bindingName == "jackson-json") {
|
||||
if (bindingName == "jackson-json")
|
||||
configuredFeatures :+ (SerializationFeature.INDENT_OUTPUT -> true)
|
||||
} else
|
||||
else
|
||||
super.overrideConfiguredSerializationFeatures(bindingName, configuredFeatures)
|
||||
}
|
||||
|
||||
override def overrideConfiguredModules(
|
||||
bindingName: String,
|
||||
configuredModules: immutable.Seq[Module]): immutable.Seq[Module] =
|
||||
if (bindingName == "jackson-json") {
|
||||
if (bindingName == "jackson-json")
|
||||
configuredModules.filterNot(_.isInstanceOf[JavaTimeModule])
|
||||
} else
|
||||
else
|
||||
super.overrideConfiguredModules(bindingName, configuredModules)
|
||||
|
||||
override def overrideConfiguredMapperFeatures(
|
||||
bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(MapperFeature, Boolean)]): immutable.Seq[(MapperFeature, Boolean)] =
|
||||
if (bindingName == "jackson-json")
|
||||
configuredFeatures :+ (MapperFeature.SORT_PROPERTIES_ALPHABETICALLY -> true)
|
||||
else
|
||||
super.overrideConfiguredMapperFeatures(bindingName, configuredFeatures)
|
||||
|
||||
override def overrideConfiguredJsonParserFeatures(
|
||||
bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(JsonParser.Feature, Boolean)])
|
||||
: immutable.Seq[(JsonParser.Feature, Boolean)] =
|
||||
if (bindingName == "jackson-json")
|
||||
configuredFeatures :+ (JsonParser.Feature.ALLOW_SINGLE_QUOTES -> true)
|
||||
else
|
||||
super.overrideConfiguredJsonParserFeatures(bindingName, configuredFeatures)
|
||||
|
||||
override def overrideConfiguredJsonGeneratorFeatures(
|
||||
bindingName: String,
|
||||
configuredFeatures: immutable.Seq[(JsonGenerator.Feature, Boolean)])
|
||||
: immutable.Seq[(JsonGenerator.Feature, Boolean)] =
|
||||
if (bindingName == "jackson-json")
|
||||
configuredFeatures :+ (JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS -> true)
|
||||
else
|
||||
super.overrideConfiguredJsonGeneratorFeatures(bindingName, configuredFeatures)
|
||||
}
|
||||
|
||||
val config = system.settings.config
|
||||
|
|
@ -275,12 +365,19 @@ class JacksonJsonSerializerSpec extends JacksonSerializerSpec("jackson-json") {
|
|||
.withSetup(JacksonObjectMapperProviderSetup(customJacksonObjectMapperFactory))
|
||||
.withSetup(BootstrapSetup(config))
|
||||
withSystem(setup) { sys =>
|
||||
val mapper = JacksonObjectMapperProvider(sys).getOrCreate("jackson-json", None)
|
||||
mapper.isEnabled(SerializationFeature.INDENT_OUTPUT) should ===(true)
|
||||
mapper.isEnabled(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) should ===(true)
|
||||
mapper.isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES) should ===(true)
|
||||
mapper.isEnabled(SerializationFeature.INDENT_OUTPUT) should ===(true)
|
||||
mapper.isEnabled(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS) should ===(true)
|
||||
|
||||
val msg = InstantCommand(Instant.ofEpochMilli(1559907792075L))
|
||||
val json = serializeToJsonString(msg, sys)
|
||||
// using the custom ObjectMapper with pretty printing enabled, and no JavaTimeModule
|
||||
json should include(""" "instant" : {""")
|
||||
json should include(""" "seconds" : 1559907792,""")
|
||||
json should include(""" "nanos" : 75000000,""")
|
||||
json should include(""" "nanos" : "75000000",""")
|
||||
json should include(""" "seconds" : "1559907792"""")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue