For our service input, we use an object mapper with certain configs to serialize it. We provide the same object mapper configs to our client in our client-lib and use the same to deserialize the input at our end.
Now we are adding another object to our input which is owned by a common team and has it's own object mapper config to correctly serialize it.
class MyAPIRequest {
MyOtherOwnedClass1 obj1;
MyOtherOwnedClass2 obj2;
//New Shared class which is being added as part of input now:
CommonlyOwnedClass newObj;
}
class MyAPIRequestObjectMapperFactory() {
static ObjectMapper newInstance(IonSystem ionSystem) {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);
return objectMapper;
}
}
class CommonlyOwnedClassObjectMapperFactory() {
static ObjectMapper newInstance(IonSystem ionSystem) {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.ANY);
objectMapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.ANY);
return objectMapper;
}
}
How can I update my ObjectMapper to use a different ObjectMapper (provided by CommonlyOwnedClassObjectMapperFactory) for newObj and continue to use existing object mapper (in MyAPIRequestObjectMapperFactory) for rest of objects in MyAPIRequest ?
Edit :
I am using Jackson-2.8, but can upgrade to 2.9 if required
The setVisibility method is controlled by an protected configuration variable (_serializationConfig & _deserializationConfig in 2.8 and _configOverrides in 2.9). The method setVisibility is overloaded which takes an visibility checker to override internal configuration variable. You can use the overloaded version to set configuration for your mapper by getting the visibility checker from external mapper.
ObjectMapper yourObjectMapper = MyAPIRequestObjectMapperFactory.newInstance();
ObjectMapper externalObjectMapper = CommonlyOwnedClassObjectMapperFactory.newInstance();
yourObjectMapper.setVisibility(externalObjectMapper.getVisibilityChecker());
//then set your visibility
Related
I'm using spring boot 2.0.3 and
spring-boot-starter-data-redis.
Also using jackson-datatype-jsr310.
I want to store Object into redis.
the object(MyObj):
String text;
Instant instant;
Here's my code:
#Test
public void test() {
ListOperations<String, MyObj> listOps = redisTemplate.opsForList();
MyObj o1 = new MyObj();
o1.setText("foo");
o1.setInstant(Instant.now());
listOps.leftPush("foo", o1);
MyObj o2 = new MyObj();
o2.setText("bar");
o2.setInstant(Instant.now());
listOps.leftPush("foo", o2);
List<MyObj> list = listOps.range("foo", 0, -1);
for (MyObj o : list) {
System.out.println(o.getText());
System.out.println(o.getInstant());
}
}
in my RedisConfig:
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
But when I'm pushing into redis, the error occurs below:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of java.time.Instant (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
How to serialize java instant type with Redis?
Any opinion would be appreciated.
While this is quite an old post, I ran into this problem recently and did the lazy man search and found this before deciding to read the class file. I found that you can easily override the default ObjectMapper with a custom one. Use the setObjectMapper(ObjectMapper objectMapper) method on the Serializer to override the default.
// Taken from Jackson library
public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private final JavaType javaType;
private ObjectMapper objectMapper = new ObjectMapper();
// truncated
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
You just need to create an ObjectMapper with the JavaTime.Module registered like below
public static ObjectMapper dateMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
Jackson2JsonRedisSerializer<MyObj> valueSerializer = new
Jackson2JsonRedisSerializer<>(MyObj.class);
valueSerializer.setObjectMapper(dateMapper());
}
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) turns of default behavior.
I'm trying to marshal response containing ISO formatted timestamp like that:
{
...
"time" : "2014-07-02T04:00:00.000000Z"
...
}
into ZonedDateTime field in my domain model object. Eventually it works if I use solution that is commented in following snippet.There are many similar questions on SO but I would like to get a specific answer
what is wrong with another approach which uses JacksonJsonProvider with ObjectMapper + JavaTimeModule?
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
JacksonJsonProvider provider = new JacksonJsonProvider(mapper);
Client client = ClientBuilder.newBuilder()
// .register(new ObjectMapperContextResolver(){
// #Override
// public ObjectMapper getContext(Class<?> type) {
// ObjectMapper mapper = new ObjectMapper();
// mapper.registerModule(new JavaTimeModule());
// return mapper;
// }
// })
.register(provider)
.register(JacksonFeature.class)
.build();
Error I get:
javax.ws.rs.client.ResponseProcessingException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of java.time.ZonedDateTime: no String-argument constructor/factory method to deserialize from String value ('2017-02-24T20:46:05.000000Z')
at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream#53941c2f
Project dependencies are:
compile 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.8.7'
compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.7'
compile 'com.fasterxml.jackson.core:jackson-core:2.8.7'
compile 'com.fasterxml.jackson.core:jackson-databind:2.8.7'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.8.7'
compile 'org.glassfish.jersey.core:jersey-client:2.25.1'
edit
Deserialization happens in here:
CandlesResponse<BidAskCandle> candlesResponse = webTarget.request()
.header(HttpHeaders.AUTHORIZATION,"Bearer "+token)
.accept(MediaType.APPLICATION_JSON)
.get(new GenericType<CandlesResponse<BidAskCandle>>(){});
Eventually it works if I use solution that is commented in following snippet.
First of all, you are missing a dependency in your list, that you also have, which is the problem.
jersey-media-json-jackson
This module depends on the native Jackson module that has the JacksonJsonProvider. When you register the JacksonFeature (that comes with jersey-media-json-jackson), it registers its own JacksonJaxbJsonProvider, which seems to take precedence over any that you provide.
When you use the ContextResolver, the JacksonJsonProvider actually looks-up that ContextResolver and uses it to resolve the ObjectMapper. That's why it works. Whether you used the JacksonFeature or registered your own JacksonJsonProvider (without configuring an ObjectMapper for it) the ContextResovler would work.
Another thing about the jersey-media-json-jackson module, it that it participates in Jersey's auto-discoverable mechanism, which registers it's JacksonFeature. So even if you didn't explicitly register it, it would still be registered. The only ways to avoid it being registered are to:
Disable the auto-discovery (as mention in the previous link)1
Don't use the jersey-media-json-jackson. Just use the Jackson native module jackson-jaxrs-json-provider. Thing about this though is that, the jersey-media-json-jackson adds a couple features on top of the the native module, so you would lose those.
Haven't tested, but it seems that if you use JacksonJaxbJsonProvider instead of JacksonJsonProvider, it might work. If you look at the source for the JacksonFeature, you will see that it checks for an already registered JacksonJaxbJsonProvider. If there is one, it won't register it's own.
The one thing I'm not sure about with this is the auto-discoverable. The order in which it is registered, if it will affect whether or not it catches your registered JacksonJaxbJsonProvider. Something you can test out.
Footnotes
1. See also
From my pet project:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
public WebTarget getTarget(URI uri) {
Client client = ClientBuilder
.newClient()
.register(JacksonConfig.class);
return client.target(uri);
}
where
#Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
public JacksonConfig() {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
#Override
public ObjectMapper getContext(Class<?> aClass) {
return objectMapper;
}
}
Jackson configuration looks fine, I tried the following and was able to deserialize the value:
public class Test {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
Model model = mapper.readValue("{\"time\" : \"2014-07-02T04:00:00.000000Z\"}", Model.class);
System.out.println(model.getTime());
}
}
class Model{
private ZonedDateTime time;
public ZonedDateTime getTime() {
return time;
}
public void setTime(ZonedDateTime time) {
this.time = time;
}
}
I can reproduce it by commenting out mapper.registerModule(new JavaTimeModule());. So, it looks like jersey client is not using custom mapper instance. Could you try configuring it as described here.
Java Model:
#lombok.Data
public class Foo {
...
private boolean isDefault;
}
Serialized to JSON:
{
...,
"isDefault" : true
}
ObjectMapper configuration:
ObjectMapper mapper = new ObjectMapper();
VisibilityChecker<?> vc = objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
.withIsGetterVisibility(JsonAutoDetect.Visibility.NONE);
mapper.setVisibility(vc);
Question: the code cannot deserialize the JSON to java object and fails with UnrecognizedPropertyException for isDefault even though it perfectly serializes it to JSON. Any thought?
Thanks in advance.
It's easy to create a custom ObjectMapper for Spring, but the configuration requires XML. I'm trying to reduce the amount of XML configuration for things that really aren't going to change without requiring a redeploy of my entire system anyway.
So the title says it all - can I use annotations or some other non-XML method to tell Spring, "Hey, please use my custom object mapper pls"?
EDIT:
This does not appear to work
#Configuration
#EnableWebMvc
public class AppConfig {
#Primary
#Bean
public ObjectMapper mapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
mapper.registerModule(new JodaModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
EDIT 2:
I do not believe that Spring is using my ObjectMapper. I have this code:
#Primary
#Bean
public ObjectMapper mapper(){
ObjectMapper mapper = new ObjectMapper();
JodaModule mod = new JodaModule();
mod.addSerializer(DateTime.class, new JsonSerializer<DateTime>() {
#Override
public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
System.out.println("Hi, bob");
}
});
mapper.registerModule(mod);
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
return mapper;
}
but when I set a breakpoint on System.out.println("Hi, bob") it is never called - Though I'm definitely serializing a DateTime.
You can always follow the steps provided in the Spring Docs.
If you want to replace the default ObjectMapper completely, define a #Bean of that type and mark it as #Primary.
Defining a #Bean of type Jackson2ObjectMapperBuilder will allow you to customize both default ObjectMapper and XmlMapper (used in MappingJackson2HttpMessageConverter and MappingJackson2XmlHttpMessageConverter respectively).
So, either you define a #Bean with your ObjectMapper like this:
#Primary
#Bean
public ObjectMapper mapper() {
// Customize...
return new ObjectMapper().setLocale(Locale.UK);
}
Or, you define a #Bean of type Jackson2ObjectMapperBuilder and customize the builder like this:
#Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
// Customize
builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd"));
return builder;
}
This blog entry describes the customization further.
In order to register beans using the #Bean annotation you must declare the #Bean in a #Configuration class as described in the Spring docs about Java-based container configuration.
This seems to be a bug. The Spring Boot documentation says that annotating an ObjectMapper Bean with #Primary should make the Spring context use it instead of the Spring's default mapper. However, this doesn't seem to work. I have found a workaround without using XML.
//Since Spring won't use the custom object mapper Bean defined below for
//HTTP message conversion(eg., when a Java object is returned from a controller,
//and should be converted to json using Jackson), we must override this method
//and tell it to use a custom message converter. We configure that custom converter
//below to use our customized Object mapper.
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(mappingJackson2HttpMessageConverter());
}
//configures the converter to use our custom ObjectMapper
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
//this line here
jsonConverter.setObjectMapper(objectMapper());
return jsonConverter;
}
//Primary annotation tells the Spring container to use this
//mapper as the primary mapper, instead of
//the Spring's defaultly configured mapper. Primary annotation
// DOESN'T work for some reason(this is most likely a bug and will be resolved in the future.
// When resolved, this Bean will be all it takes to tell Spring to use this ObjectMapper everywhere)
// That means that there won't be a need to configure the Http message converters manually(see method above).
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
configureObjectMapper(mapper);
return mapper;
}
//configure ObjectMapper any way you'd like
//This configuration tells the ObjectMapper to
//(de)serialize all fields(private,protected,public,..) of all objects
//and to NOT (de)serialize any properties(getters,setters).
private void configureObjectMapper(ObjectMapper mapper) {
//properties for jackson are fields with getters and setters
//sets all properties to NOT be serialized or deserialized
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
//tell the mapper to traverse all fields and not only default
//default=public fields + fields with getters and setters
//set all fields to be serialized and deserialized
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
}
MappingJackson2HttpMessageConverter objConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objMapper = new ObjectMapper();
objMapper.setVisibility(PropertyAccessor.FIELD, Visibility.NONE);
objMapper.getSerializationConfig().withView(View.class);
objConverter.setObjectMapper(objMapper);
objConverter.getObjectMapper().getSerializationConfig().withView(View.class);
after
objMapper.getSerializationConfig()
has different reference than SerializationConfig created with method (..).withView(..)
It look as if new SerializationConfig is not applied to objMapper
Hot to solve this issue? Also my #JsonView annotations are not working.
fasterxml.jackson 2.3.0
Based on the linked answer by Javier Molla:
You should use .configure() on your ObjectMapper and preferably reuse the mapper.
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false);
mapper.setSerializationInclusion(Include.NON_NULL);
You can put the serialization features in a static block and reuse the mapper => performance boost.