Jackson Integration¶
Finatra builds upon the TwitterUtil Jackson library for JSON which provides a c.t.util.jackson.ScalaObjectMapper. See the User Guide for details about the TwitterUtil Jackson library.
🚨 This documentation assumes some level of familiarity with Jackson JSON Processing library
Specifically Jackson databinding and the Jackson ObjectMapper.
There are several tutorials (and external documentation) which may be useful if you are unfamiliar with Jackson.
Additionally, you may want to familiarize yourself with the Jackson Annotations as they allow for finer-grain customization of Jackson databinding.
Case Classes¶
As mentioned, Jackson is a JSON processing library. We generally use Jackson for databinding, or more specifically:
object serialization: converting an object into a JSON String and
object deserialization: converting a JSON String into an object
The TwitterUtil Jackson integration is primarily centered around serializing and deserializing Scala case classes. This is because Scala case classes map well to the two JSON structures [reference]:
A collection of name/value pairs. Generally termed an object. Here, the name/value pairs are the case field name to field value but can also be an actual Scala Map[T, U] as well.
An ordered list of values. Typically an array, vector, list, or sequence, which for case classes can be represented by a Scala Iterable.
Basic Usage¶
Here we outline some simple usage patterns. More examples can be found in the TwitterUtil User Guide.
Let’s assume we have these two case classes:
case class Bar(d: String)
case class Foo(a: String, b: Int, c: Bar)
To serialize a case class into a JSON string, use
ScalaObjectMapper#writeValueAsString(any: Any): String
For example:
Welcome to Scala 2.12.13 (JDK 64-Bit Server VM, Java 1.8.0_242).
Type in expressions for evaluation. Or try :help.
scala> import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
scala> import com.twitter.util.jackson.ScalaObjectMapper
import com.twitter.util.jackson.ScalaObjectMapper
scala> val mapper: ScalaObjectMapper = (new ScalaObjectMapperModule).objectMapper
mapper: com.twitter.util.jackson.ScalaObjectMapper = com.twitter.util.jackson.ScalaObjectMapper@5690c2a8
scala> val foo = Foo("Hello, World", 42, Bar("Goodbye, World"))
foo: Foo = Foo(Hello, World,42,Bar(Goodbye, World))
scala> mapper.writeValueAsString(foo)
res0: String = {"a":"Hello, World","b":42,"c":{"d":"Goodbye, World"}}
scala> // or use the configured "pretty print mapper"
scala> mapper.writePrettyString(foo)
res1: String =
{
"a" : "Hello, World",
"b" : 42,
"c" : {
"d" : "Goodbye, World"
}
}
scala>
To deserialize a JSON string into a case class, use
ScalaObjectMapper#parse[T](s: String): T
For example, assuming the same Bar and Foo case classes defined above:
Welcome to Scala 2.12.13 (JDK 64-Bit Server VM, Java 1.8.0_242).
Type in expressions for evaluation. Or try :help.
scala> import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
scala> import com.twitter.util.jackson.ScalaObjectMapper
import com.twitter.util.jackson.ScalaObjectMapper
scala> val mapper: ScalaObjectMapper = (new ScalaObjectMapperModule).objectMapper
mapper: com.twitter.util.jackson.ScalaObjectMapper = com.twitter.util.jackson.ScalaObjectMapper@2c1523cd
scala> val s = """{"a": "Hello, World", "b": 42, "c": {"d": "Goodbye, World"}}"""
s: String = {"a": "Hello, World", "b": 42, "c": {"d": "Goodbye, World"}}
scala> val foo = mapper.parse[Foo](s)
foo: Foo = Foo(Hello, World,42,Bar(Goodbye, World))
scala>
You can find many examples of using the ScalaObjectMapperModule in the various framework tests:
As mentioned above, there is also a plethora of Jackson tutorials and HOW-TOs available online which provide more in-depth examples of how to use a Jackson ObjectMapper.
TwitterUtil ScalaObjectMapper¶
The TwitterUtil ScalaObjectMapper is a thin wrapper around a configured jackson-module-scala com.fasterxml.jackson.module.scala.ScalaObjectMapper. In Finatra, the recommended way to construct a ScalaObjectMapper is via the Finatra ScalaObjectMapperModule.
c.t.f.jackson.modules.ScalaObjectMapperModule¶
Finatra provides the ScalaObjectMapperModule, a c.t.inject.TwitterModule, which is the recommended way to configure and bind a ScalaObjectMapper to the object graph. This is similar to the jackson-module-guice ObjectMapperModule but uses Finatra’s TwitterModule.
The ScalaObjectMapperModule provides bound instances of:
a JsonFactory configured ScalaObjectMapper as a Singleton.
a JsonFactory ScalaObjectMapper with a PropertyNamingStrategy of camelCase as a Singleton.
a JsonFactory ScalaObjectMapper with a PropertyNamingStrategy of snake_case as a Singleton.
Tip
Generally, you are encouraged to obtain a reference to the Singleton instance provided by the object graph over instantiating a new mapper. This is to ensure usage of a consistently configured mapper across your application.
The ScalaObjectMapperModule provides overridable methods which mirror the ScalaObjectMapper#Builder for configuring the bound mappers.
For example, to create a c.t.inject.TwitterModule which sets the PropertyNamingStrategy different than the default:
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
object MyCustomObjectMapperModule extends ScalaObjectMapperModule {
override val propertyNamingStrategy: PropertyNamingStrategy =
new PropertyNamingStrategy.KebabCaseStrategy
}
Or to set additional modules or configuration:
import com.fasterxml.jackson.databind.{
DeserializationFeature,
Module,
ObjectMapper,
PropertyNamingStrategy
}
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
object MyCustomObjectMapperModule extends ScalaObjectMapperModule {
override val propertyNamingStrategy: PropertyNamingStrategy =
new PropertyNamingStrategy.KebabCaseStrategy
override val additionalJacksonModules: Seq[Module] =
Seq(MySimpleJacksonModule)
override def additionalMapperConfiguration(mapper: ObjectMapper): Unit = {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
}
}
See the Modules Configuration in Servers or the HTTP Server Framework Modules for more information on how to make use of any custom ScalaObjectMapperModule.
c.t.f.jackson.modules.YamlScalaObjectMapperModule¶
Finatra also provides the YamlScalaObjectMapperModule which is an extension of the ScalaObjectMapperModule that can be used to configure and bind a ScalaObjectMapper using a Jackson YAMLFactory instead of a JsonFactory.
The YamlScalaObjectMapperModule provides bound instances of:
a YAMLFactory configured ScalaObjectMapper as a Singleton.
a YAMLFactory configured ScalaObjectMapper with a PropertyNamingStrategy of camelCase as a Singleton.
a YAMLFactory configured ScalaObjectMapper with a PropertyNamingStrategy of snake_Case as a Singleton.
Since the YamlScalaObjectMapperModule is an extension of the ScalaObjectMapperModule is can be used in place of the ScalaObjectMapperModule where a YAML object mapper is desired.
Adding a Custom Serializer or Deserializer¶
To register a custom serializer or deserializer, configure any custom serializer or deserializer via the methods provided by the ScalaObjectMapperModule.
Create a new Jackson com.fasterxml.jackson.databind.JacksonModule implementation.
Tip
To implement a new Jackson Module for adding a basic custom serializer or deserializer, you can use the com.fasterxml.jackson.databind.module.SimpleModule.
Note, that if you want to register a JsonSerializer or JsonDeserializer over a parameterized type, such as a Collection[T] or Map[T, U], that you should instead implement com.fasterxml.jackson.databind.deser.Deserializers or com.fasterxml.jackson.databind.ser.Serializers which provide callbacks to match the full signatures of the class to deserialize into via a Jackson JavaType.
Also note that with this usage it is generally recommended to add your Serializers or Deserializers implementation via a jackson-module-scala JacksonModule. (which is an extension of com.fasterxml.jackson.databind.JacksonModule and can thus be used in place). See example below.
Add your serializer or deserializer using the SimpleModule#addSerializer or SimpleModule#addDeserializer methods in your module.
In your custom ScalaObjectMapperModule extension, add the JacksonModule implementation to list of additional Jackson modules by overriding and implementing the ScalaObjectMapperModule#additionalJacksonModules.
For example: first create the serializer or deserializer (a deserializer example is shown below)
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.Deserializers
// custom deserializer
class FooDeserializer extends JsonDeserializer[Foo] {
override def deserialize(...)
}
// custom parameterized deserializer
class MapIntIntDeserializer extends JsonDeserializer[Map[Int, Int]] {
override def deserialize(...)
}
// custom parameterized deserializer resolver
class MapIntIntDeserializerResolver extends Deserializers.Base {
override def findBeanDeserializer(
javaType: JavaType,
config: DeserializationConfig,
beanDesc: BeanDescription
): MapIntIntDeserializer = {
if (javaType.isMapLikeType && javaType.hasGenericTypes && hasIntTypes(javaType)) {
new MapIntIntDeserializer
} else null
}
private[this] def hasIntTypes(javaType: JavaType): Boolean = {
val k = javaType.containedType(0)
val v = javaType.containedType(1)
k.isPrimitive && k.getRawClass == classOf[Integer] &&
v.isPrimitive && v.getRawClass == classOf[Integer]
}
}
Then add via a Jackson SimpleModule or a jackson-module-scala JacksonModule:
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.scala.JacksonModule
// Jackson SimpleModule for custom deserializer
class FooDeserializerModule extends SimpleModule {
addDeserializer(FooDeserializer)
}
// Jackson Module Scala JacksonModule for custom deserializer
class MapIntIntDeserializerModule extends JacksonModule {
override def getModuleName: String = this.getClass.getName
this += {
_.addDeserializers(new MapIntIntDeserializerResolver)
}
}
Note
It is also important to note that Jackson Modules are not Google Guice Modules but are instead interfaces for extensions that can be registered with a Jackson ObjectMapper in order to provide a well-defined set of extensions to default functionality. In this way, they are similar in concept to Google Guice Modules, but for configuring an ObjectMapper instead of an Injector.
Lastly, add the custom serializer or deserializer to your customized Finatra ScalaObjectMapperModule:
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
object MyCustomObjectMapperModule extends ScalaObjectMapperModule {
override val additionalJacksonModules = Seq(
// added via a new anonymous SimpleModule
new SimpleModule {
addSerializer(LocalDateParser)
},
// added via a re-usable SimpleModule
new FooDeserializerModule,
// added via a re-usable JacksonModule
new MapIntIntDeserializerModule
)
}
For more information see the Jackson documentation for Custom Serializers.
You would then use this module in your server. See the Modules Configuration in Servers or the HTTP Server Framework Modules for more information on how to make use of any custom ScalaObjectMapperModule.
Improved case class deserializer¶
The TwitterUtil ScalaObjectMapper provides a custom case class deserializer which overcomes some limitations in jackson-module-scala and it is worth understanding some its features:
Throws a JsonMappingException when required fields are missing from the parsed JSON.
Uses specified case class default values when fields are missing in the incoming JSON.
Properly deserializes a Seq[Long] (see: https://github.com/FasterXML/jackson-module-scala/issues/62).
Supports “wrapped values” using c.t.util.jackson.WrappedValue.
Support for field and method level validations via integration with the util-validator Bean Validation 2.0 style validations during JSON deserialization.
Accumulates all JSON deserialization errors (instead of failing fast) in a returned sub-class of JsonMappingException (see: CaseClassMappingException).
The case class deserializer is added by default when constructing a new ScalaObjectMapper.
Tip
Note: with the TwitterUtil case class deserializer, non-option fields without default values are considered required.
If a required field is missing, a CaseClassMappingException is thrown.
Jackson InjectableValues Support¶
By default, the Finatra provides a ScalaObjectMapper via the ScalaObjectMapperModule configured to resolve Jackson InjectableValues via a given Google Guice Injector.
The default is very similar to the jackson-module-guice – GuiceInjectableValues.
Note
Jackson “InjectableValues” is not related to Dependency Injection or Google Guice. It is meant to convey the filling in of a value in a deserialized object from somewhere other than the incoming JSON. In Jackson parlance, this is “injection” of a value.
The Finatra c.t.finatra.jackson.caseclass.GuiceInjectableValues allows users to denote fields in the case class to fill with values that come from a configured Google Guice Injector such that you can do this:
import javax.inject.Inject
case class Foo(name: String, description: String, @Inject bar: Bar)
That is, annotate the field to inject with either:
and the framework will attempt to get an instance of the field type from the Injector with which the mapper was configured. In this case, the framework would attempt to obtain an instance of Bar from the object graph.
As mentioned, this is essentially the same functionality found in the jackson-module-guice GuiceInjectableValues with a key difference being that the Finatra version ensures we do not attempt lookup of a field from the Guice Injector unless there is a non-null Guice Injector configured and the field is specifically annotated with one of the supporting annotations.
Using the Foo case class above, you could then parse incoming JSON with the ScalaObjectMapper:
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
import com.twitter.util.jackson.ScalaObjectMapper
import com.twitter.inject.Injector
import com.twitter.inject.app.TestInjector
import javax.inject.Inject
case class Foo(name: String, description: String, @Inject bar: Bar)
val json: String =
"""
|{
| “name”: “FooItem”,
| “description”: “This is the description for FooItem”
|}
""".stripMargin
val injector: Injector = TestInjector(???).create
val mapper: ScalaObjectMapper = (new ScalaObjectMapper).objectMapper(injector.underlying)
val foo = mapper.parse[Foo](json)
When deserializing the JSON string into an instance of Foo, the mapper will attempt to locate an instance of type Bar from the given injector and use it in place of the bar field in the Foo case class.
Caution
It is an error to specify multiple field injection annotations on a field, and it is also an error to use a field injection annotation in conjunction with any JacksonAnnotation.
Both of these cases will result in error during deserialization of JSON into the case class when using the ScalaObjectMapper.
As mentioned, the Finatra HTTP integration provides extended Jackson InjectableValues support specifically for injecting values into a case class which can be obtained from different parts of an HTTP message. This uses the TwitterUtil @InjectableValues annotation on specific Finatra HTTP request related annotations.
See the HTTP Requests - Field Annotations documentation for more details on HTTP Message “injectable values”.
Important
It is important to note that Jackson currently only allows registration of a single InjectableValues implementation for an ObjectMapper.
Thus, if you configure your bound ScalaObjectMapper with a different implementation, the default behavior provided by the Finatra GuiceInjectableValues will be overridden.
Mix-in Annotations¶
The Jackson Mix-in Annotations provides a way to associate annotations to classes without needing to modify the target classes themselves. It is intended to help support 3rd party datatypes where the user cannot modify the sources to add annotations.
The TwitterUtil ScalaObjectMapper supports Jackson Mix-in Annotations for specifying field annotations during deserialization with the case class deserializer.
For example, to deserialize JSON into the following classes that are not yours to annotate:
case class Point(x: Int, y: Int) {
def area: Int = x * y
}
case class Points(points: Seq[Point])
However, you want to enforce field constraints with validations during deserialization. You can define a Mix-in,
trait PointMixIn {
@Min(0) @Max(100) def x: Int
@Min(0) @Max(100) def y: Int
@JsonIgnore def area: Int
}
Then register this Mix-in for the Point class type. There are several ways to do this. Generally, it is recommended to always prefer applying configuration in a custom ScalaObjectMapperModule to ensure usage of a consistently configured mapper across your application.
Implement via a Custom ScalaObjectMapperModule¶
First, create a new Jackson com.fasterxml.jackson.databind.JacksonModule implementation. You can use the com.fasterxml.jackson.databind.module.SimpleModule.
Add your MixIn using the SimpleModule#setMixInAnnotation method in your module.
In your custom ScalaObjectMapperModule extension, add the JacksonModule.
For example, create a new Jackson SimpleModule:
import com.fasterxml.jackson.databind.module.SimpleModule
object PointMixInModule extends SimpleModule {
setMixInAnnotation(classOf[Point], classOf[PointMixIn]);
}
Then add the SimpleModule to the list of additional Jackson modules in your custom ScalaObjectMapperModule:
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
object MyCustomObjectMapperModule extends ScalaObjectMapperModule {
override val additionalJacksonModules = Seq(PointMixInModule)
}
Deserializing this JSON would then error with failed validations:
{
"points": [
{"x": -1, "y": 120},
{"x": 4, "y": 99}
]
}
As the first Point instance has an x-value less than the minimum of 0 and a y-value greater than the maximum of 100.