I've got a class that configures a Jackson ObjectMapper. It adds in some custom serializers and deserializers for my object types as follows:
public class JsonMapperFactory {
public static ObjectMapper createObjectMapper() {
final SimpleModule module = new SimpleModule("customerSerializationModule", new Version(1, 0, 0, "static version"));
addCustomDeserializersTo(module);
addCustomSerializersTo(module);
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);
return objectMapper;
}
private static void addCustomSerializersTo(final SimpleModule module) {
module.addSerializer(DateTime.class, new DateTimeSerializer());
}
private static void addCustomDeserializersTo(final SimpleModule objectMapper) {
objectMapper.addDeserializer(DateTime.class, new DateTimeDeserializer());
}
}
I've tested my customer serializers within their own test classes, so in my test of this JsonMapperFactory class, I'm trying to simply check that the ObjectMapper created has the expected serializers (or deserializers) This could be achieve by introspecting the ObjectMapper, but it doesn't seem to have any mechanisms to do this.
Does anyone know of a nice way to test that?
For deserializers, I have the following:
private void assertThatObjectMapperUsesCorrectDeserializer(final Class<?> typeClazz, final Class<?> deserializerClazz) throws JsonMappingException {
final DeserializationConfig deserializationConfig = this.objectMapper.getDeserializationConfig();
final JsonDeserializer<Object> deserializer = this.objectMapper.getDeserializerProvider().findTypedValueDeserializer(deserializationConfig, javaTypeFor(typeClazz), null);
assertThat(deserializer, is(instanceOf(deserializerClazz)));
}
private JavaType javaTypeFor(final Class<?> clazz) {
return TypeFactory.type(clazz); //deprecated method :(
}
Which is quite verbose and uses deprecated methods.
I'm yet to find a way to do a similar test for the serializers. So I've currently resorted to serializing an object and check it serializes correctly (essentially duplicating the serializer test)
Any ideas are very welcome.
From the answers & comments provided here, I recently redesigned the class to use builders for both the Module and the ObjectMapper. This allowed me to provide mocks and check that the correct (de)serializers were added to the module and then the module is registered to the object mapper as expected.
Object Mapper Builder:
public class ObjectMapperBuilder {
ObjectMapper mapper;
public ObjectMapperBuilder configure(final ObjectMapper mapper) {
this.mapper = mapper;
return this;
}
public ObjectMapperBuilder withModule(final Module module) {
this.mapper.registerModule(module);
return this;
}
public ObjectMapper build() {
return this.mapper;
}
}
Module Builder:
public class SimpleModuleBuilder {
SimpleModule module;
public SimpleModuleBuilder configure(final SimpleModule module) {
this.module = module;
return this;
}
public <X> SimpleModuleBuilder withSerializer(final Class<X> clazz, final JsonSerializer<X> serializer) {
this.module.addSerializer(clazz, serializer);
return this;
}
public <X> SimpleModuleBuilder withDeserializer(final Class<X> clazz, final JsonDeserializer<X> deserializer) {
this.module.addDeserializer(clazz, deserializer);
return this;
}
public SimpleModule build() {
return this.module;
}
}
And finally, the new JsonMapperFactory:
public class JsonMapperFactory {
public static ObjectMapper configureObjectMapper(final ObjectMapper mapper, final SimpleModule module) {
final SimpleModuleBuilder modulebuilder = new SimpleModuleBuilder();
final SimpleModule configuredModule = modulebuilder.configure(module)
.withSerializer(DateTime.class, new DateTimeSerializer())
.withDeserializer(DateTime.class, new DateTimeDeserializer())
.build();
final ObjectMapperBuilder objectMapperBuilder = new ObjectMapperBuilder();
return objectMapperBuilder.configure(mapper).withModule(configuredModule).build();
}
}
The factory method is still used within Spring configuration, but the configuration now instantiates the blank Module and ObjectMapper before providing them to the factory methods that then configure them.
If JsonDeserializer (and DateTimeDeserializer too) was an interface, you could easily "JMock" it, pass mocked instance to JsonMapperFactory#createObjectMapper and then expect exactly 1 invocation of your custom "serialize" method; e.g.
DateTimeSerializer serializer = context.mock(DateTimeSerializer.class);
DateTimeDeserializer serializer = context.mock(DateTimeDeserializer.class);
ObjectMapper mapper = JacksonMapperFactory.createObjectMapper(deserializer, serializer);
exactly(1).of(jsonDeserializer).serialize(myDateTime,
with(any(JsonGenerator.class),
with(any(SerializerProvider.class)))
Being a concrete class, you can instead define a new (test-scoped) De/Serializer that extends your custom DateTime(De)serializer, and simply count invocation on that:
private static class DateTimeDeserializerWithCounter extends DateTimeDeserializer {
public int counter = 0;
#Override
public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
counter++;
return super.deserialize(jsonParser, deserializationContext);
}
}
#Test
public void usageTest(){
//init mapper with the above DateTimeDeserializerWithCounter - see below
mapper.readValue("...", DateTime.class);
Assert.assertEquals(1, deserializer.counter);
}
Below a snapshot of a more "test-oriented" Factory:
//package visibility, to allow passing different De/Serializers while testing
static ObjectMapper createObjectMapper(JsonDeserializer deserializer, JsonSerializer serializer) {
final SimpleModule module = new SimpleModule("customerSerializationModule", new Version(1, 0, 0, "static version"));
module.addDeserializer(DateTime.class, deserializer);
module.addSerializer(DateTime.class, serializer);
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);
return objectMapper;
}
//production method: no-args, as in the original version
public static ObjectMapper createObjectMapper() {
return createObjectMapper(new DateTimeDeserializer(), new DateTimeSerializer());
}
Hope that helps.
Related
Currently I am registering my class with a custom JsonDeserialize class with declaring a new SimpleModule in my app start, something like
public static void configure(ObjectMapperModule mapper) {
SimpleModule module = new SimpleModule();
module.addDeserializer(LocalFile.class, new LocalFileDeserializer());
// module add more deserializer class
mapper.registerModule(module);
}
Is there anyway that I can create a class that extends the SimpleModule, so I can register all class in the constructor maybe? I have tried below but it failed,
public class MyCustomModule extends SimpleModule {
private static final SimpleModule module = new SimpleModule();
public MyCustomModule() {
initMyCustomModule();
}
final void initMyCustomModule() {
module.addDeserializer(LocalFile.class, new LocalFileDeserializer());
}
}
// In app start
mapper.registerModule(new MyCustomModule());
I have seen the initMyCustomModule was being called during app start but my deserialize class was not being called when tried to deserialize my object from request, I was thinking because I should register the module inside my class, e.g: mapper.registerModule(new MyCustomModule().module)? I also noticed there was a override method for
#Override
public void setupModule(SetupContext context)
but I am not sure how to use it to addDeserializer into the context...
Seems like this works...I don't need to declare a new instance of SimpleModule and use that in constructor, I can directly use addDeserializer in my constructor..
public class MyCustomModule extends SimpleModule {
public MyCustomModule() {
initMyCustomModule();
}
final void initMyCustomModule() {
addDeserializer(LocalFile.class, new LocalFileDeserializer());
}
}
I'm trying to figure how to explain Jersey and Jackson how to deserialize a Future that I pass as byte[].
I create my own ContextResolver
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private static ObjectMapper mapper = null;
public ObjectMapperContextResolver() {
mapper = ObjectMapperFactory.getObjectMapper();
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
}
And the implementation of the ObjectMapper
public static ObjectMapper getObjectMapper() {
ObjectMapper defaultObjectMapper = new ObjectMapper();
SimpleModule futureModule = new SimpleModule("FutureModule");
futureModule.<Future>addDeserializer(Future.class, new FutureDeserializer<String>());
defaultObjectMapper.registerModule(futureModule);
return defaultObjectMapper;
}
And then finally in the implementation of my FutureDeserializer
public class FutureDeserializer<T> extends StdDeserializer<Future<T>>{
public FutureDeserializer() {
super(Future.class);
}
#Override
public Future<T> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
ObjectMapper mapper=(ObjectMapper)jp.getCodec();
//TODO: Breakpoint never stop here
return null;
}
}
Then I register in my ResourceConfig before start the JerseyTest
ResourceConfig rc = new ResourceConfig();
rc.register(SpringLifecycleListener.class);
rc.register(RequestContextFilter.class);
rc.register(new JacksonFeature());
rc.register(new ObjectMapperContextResolver());
But when I run the test the ObjectMapperContextResolver is invoked and the mapper returned to Jersey, but he never use the FutureDeserializer.
Any idea what I´m doing wrong?
When I'm trying to deserialize Object from a String and this String does not contain certain fields or has fields that are not in my Object, Jackson serializer is completely okay with that and just creates my Object with null/Optional.empty() fields, also ignoring unkown properties. I tried to set reader with feature FAIL_ON_UNKNOWN_PROPERTIES but to no success. I have fairly simple Jackson configuration, not much besides adding support for Java 8 and java.time.
Edit:
public final ObjectReader reader;
public final ObjectWriter writer;
private JsonMapperTestInstance() {
ObjectMapper mapper = new JacksonConfiguration().objectMapper();
reader = mapper.reader();
writer = mapper.writer().withFeatures(SerializationFeature.INDENT_OUTPUT);
}
public <T> T deserialize(Class<T> actual, String serialized) throws IOException {
return reader.forType(actual).readValue(serialized);
}
JacksonConfiguration:
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
registerModules(mapper);
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
return mapper;
}
#Bean
public JavaTimeModule javaTimeModule() {
return new JavaTimeModule();
}
#Bean
public Jdk8Module jdk8Module() {
return new Jdk8Module().configureAbsentsAsNulls(true);
}
private void registerModules(ObjectMapper mapper) {
mapper.registerModule(jdk8Module());
mapper.registerModule(javaTimeModule());
}
#Primary
#Bean
public ObjectWriter writer(ObjectMapper mapper) {
return mapper.writer();
}
#Primary
#Bean
public ObjectReader reader(ObjectMapper mapper) {
return mapper.reader();
}
I have determined that annotation #JasonUnwrapped is causing this behaviour. Without it Jackson throws expection on property "very_wrong_field", which previously was silently ignoring.
I have a jersey2 application configured for JSON support via Jackson, adding
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
</dependency>
in the POM file and
public MyApplication() {
...
register(JacksonFeature.class)
...
}
in my application. Everything works, my resources get deserialized POJOs as arguments
#POST #Consumes(MediaType.APPLICATION_JSON)
public void blah(MyPojo p) {
...
}
Now one of thoese resources needs a reference to Jackson's ObjectMapper to do some deserialization on its own. I've tried doing something like
#Inject
public MyResource(#Context ObjectMapper mapper) {
...
}
or
#GET
public String foo(#Context ObjectMapper mapper) {
...
}
but in both cases the reference to mapper is null. How can I inject a reference to the ObjectMapper in my resources?
First there is no default ObjectMapper used by the Jackson provider. It doesn't use an ObjectMapper at all actually. It makes use of other Jackson APIs to handle the (de)serialization.
If you want to use/inject a single ObjectMapper instance, then you should just create a Factory for it
public class ObjectMapperFactory implements Factory<ObjectMapper> {
final ObjectMapper mapper = new ObjectMapper();
#Override
public ObjectMapper provide() {
return mapper;
}
#Override
public void dispose(ObjectMapper t) {}
}
Then bind it
register(new AbstractBinder(){
#Override
public void configure() {
bindFactory(ObjectMapperFactory.class)
.to(ObjectMapper.class).in(Singleton.class);
}
});
One thing should be noted is that any configuration of the ObjectMapper is not thread safe. So say you tried to configure it from your resource method, those operations are not thread safe.
Another thing to note with the Jackson provider, is that if we provide a ContextResolver, like mentioned by #Laurentiu L, then the Jackson provider will switch to using our ObjectMapper. In which case, if you want to use that same ObjectMapper, then you can look it up in the Factory. For example
public class ObjectMapperFactory implements Factory<ObjectMapper> {
private final Providers providers;
final ObjectMapper mapper = new ObjectMapper();
public ObjectMapperFactory(#Context Providers providers) {
this.providers = providers;
}
#Override
public ObjectMapper provide() {
ContextResolver<ObjectMapper> resolver = providers.getContextResolver(
ObjectMapper.class, MediaType.APPLICATION_JSON);
if (resolver == null) { return mapper; }
return resolver.getContext(null);
}
#Override
public void dispose(ObjectMapper t) {}
}
For the above to work (use a single ObjectMapper), you need to make sure to implement the ContextResolver<ObjectMapper>, and make sure to annotation the ContextResolver with the corresponding #Produces and #Consumes media types.
Aside from the JacksonFeature you need to register a ContextResolver for ObjectMapper.
Simple example from the Documentation at 9.1.4.2. Configure and register
#Provider
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {
final ObjectMapper defaultObjectMapper;
public MyObjectMapperProvider() {
defaultObjectMapper = createDefaultMapper();
}
#Override
public ObjectMapper getContext(Class<?> type) {
return defaultObjectMapper;
}
private static ObjectMapper createDefaultMapper() {
final ObjectMapper result = new ObjectMapper();
result.configure(Feature.INDENT_OUTPUT, true);
return result;
}
// ...
}
Complete code example
available on Github
You will also need to register it
.register(MyObjectMapperProvider.class)
I'm using Jersey with Spring for a REST API and I wrote a provider to modify JSON serialization. The problem is that when I use the #Component annotation, the provider's callback method is called for other servlets. When I remove the #Component annotation, it doesn't get called at all.
Here is the provider:
#Component
#Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
public ObjectMapperProvider() {
}
#Override
public ObjectMapper getContext(Class<?> type) {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule("SimpleModule", new org.codehaus.jackson.Version(1, 0, 0, null));
module.addSerializer(BigInteger.class, new ToStringSerializer());
objectMapper.registerModule(module);
return objectMapper;
}
}
I tried to use the Jersey configuration in the web.xml, but that didn't help either.
Any ideas?
Apparently, that wasn't my problem. The provider was called for the right servlet. My application didn't work because I had an XmlAdapter for Map, and with my ObjectMapperProvider, the json response was different.
Here is my updated ObjectMapperProvider class:
#Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
#Context
UriInfo uriInfo;
public ObjectMapperProvider() {
}
#Override
public ObjectMapper getContext(Class<?> type) {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule("SimpleModule", new org.codehaus.jackson.Version(1, 0, 0, null));
module.addSerializer(BigInteger.class, new ToStringSerializer());
objectMapper = objectMapper.configure(Feature.WRAP_ROOT_VALUE, false).configure(DeserializationConfig.Feature.UNWRAP_ROOT_VALUE, false)
.configure(Feature.WRAP_EXCEPTIONS, true).configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, true).configure(Feature.WRITE_EMPTY_JSON_ARRAYS, false);
final AnnotationIntrospector introspector = new JaxbAnnotationIntrospector();
objectMapper.getDeserializationConfig().setAnnotationIntrospector(introspector); // using a deprecated API that works. Non-deprecated API doesn't work...
objectMapper.getSerializationConfig().setAnnotationIntrospector(introspector);
objectMapper.registerModule(module);
return objectMapper;
}
}
Once I configured my Object wrapper to use JAXB annotations, everything worked as expected. I got the idea on how to do that from the following post