Jackson: Use default (de)serializer - java

I'm trying to (de)serialize an object that has a property with a type that comes from a maven dependency, so I can't change the class of this type.
The class of this type has a #JsonSerialize and #JsonDeserialize annotation.
However, I want to use the default serializer and deserialzer, because the custom serializer writes an array instead of an object. Is there a way, using annotations, to tell jackson to use the default (de)serializer?

You can disable the annotations using Jackson's mixins feature.
In the following example, any attempt at deserializing to a CustomerObj will result in an exception due to its defective Builder:
#JsonDeserialize(builder = CustomerObj.class)
public class CustomerObj {
public String name;
public int age;
public CustomerObj build() {
throw new RuntimeException("JsonDeserializer invoked");
}
}
Create a mixin with a JsonDeserialize annotation that disables the broken builder:
#JsonDeserialize(builder = java.lang.Void.class)
public static abstract class CustomerMixin { }
Register the mixin on the ObjectMapper instance:
ObjectMapper om = new ObjectMapper();
om.addMixIn(CustomerObj.class, CustomerMixin.class);
Enjoy working deserialization:
final String json = "{\"name\":\"Brian\",\"age\":41}";
CustomerObj customer = om.readValue(json, CustomerObj.class);

Related

Swap Jackson custom serializer / deserializer during runtime

I have the following system:
I am sending MediaType.APPLICATION_JSON_VALUEs from spring controllers to my client and vice versa.
I also have an export/import feature of my to-be-serialized classes. The JSON File is created by using an ObjectMapper and utilizing the writeValueAsString and readValue methods. I am reading from and writing into the json file.
Both of those serialization paths currently utilize the same serializers/deserializers.
I use the #JsonSerialize and #JsonDeserialize annotations to define custom serialization for some of my objects.
I want to serialize those objects differently for export/import.
So I want to swap the serializer / deserializer for the export/import task. Something like this:
If I understand the docs correctly, those two annotations only allow one using class. But I want to register multiple serializers/deserializers and use them based on some conditional logic.
You might want to have two separate ObjectMapper instances configured for Server and Client.
Server module:
ObjectMapper serverMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ServerDTO.class, new CustomerFileSerializer());
module.addDeserializer(ServerDTO.class, new CustomerFileDeserializer());
serverMapper.registerModule(module);
ServerDTO serverDto = serverMapper.readValue(jsonInput, ServerDTO.class);
String serialized = serverMapper.writeValueAsString(serverDto);
and
Client module:
ObjectMapper clientMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClientDTO.class, new CustomerClientSerializer());
module.addDeserializer(ClientDTO.class, new CustomerClientDeserializer());
clientMapper.registerModule(module);
ClientDTO clientDTO = clientMapper.readValue(jsonInput, ClientDTO.class);
String serialized = clientMapper.writeValueAsString(clientDTO);
So I was trying to figure this out for the last few days. This is the progress I made so far:
I did two overrides for the default ObjectMapper in Spring and made sure they are configured like the default.
My custom mappers look like this:
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper defaultV7ObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerClientSerializer());
module.addDeserializer(Customer.class, new CustomerClientDeserializer());
objectMapper.registerModule(module);
return objectMapper;
}
#Bean("exportImportMapper")
public ObjectMapper exportImportMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerFileSerializer());
module.addDeserializer(Customer.class, new CustomerFileDeserializer());
objectMapper.registerModule(module);
return objectMapper;
}
}
I also removed the #JsonSerialize and #JsonDeserialize annotations from my entities.
HOWEVER there is one big difference with this change from annotations to adding the serializers via the module.
Let's say I have a class A that has a Customer property with the #JsonSerialize and #JsonDeserialize annotation.
Let's also say I have a class B that has a Customer property without annotations.
By removing the annotations and setting the serializer/deserializer as shown above I have now added theses serializers/deserializers to both Customer properties. So it's not equivalent.
Or am I missing something here?
This is my solution
It's not pretty but does its job.
I left my old jackson config untouched, so the client<->server serialization stays the same.
I then added this custom ObjectMapper to take care of my server<->file.
My custom ObjectMapper does the following things:
It registers a new custom JacksonAnnotationIntrospector, which I configured to ignore certain annotations. I also configured it to use my selfmade annotation #TransferJsonTypeInfo whenever a property has both the #TransferJsonTypeInfo as well as the #JsonTypeInfo annotation.
I registered my CustomerFileSerializer and CustomerFileDeserializer for this ObjectMapper.
#Service
public class ImportExportMapper {
protected final ObjectMapper customObjectMapper;
private static final JacksonAnnotationIntrospector IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO = BuildImportExportJacksonAnnotationIntrospector();
public ImportExportMapper(){
customObjectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
customObjectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
customObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerFileSerializer());
module.addDeserializer(Customer.class, new CustomerFileDeserializer());
customObjectMapper.setAnnotationIntrospector(IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO);
customObjectMapper.registerModule(module);
}
public String writeValueAsString(Object data) {
try {
return customObjectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new IllegalArgumentException();
}
}
public ObjectTransferData readValue(String fileContent, Class clazz) throws JsonProcessingException {
return customObjectMapper.readValue(fileContent, clazz);
}
private static JacksonAnnotationIntrospector BuildImportExportJacksonAnnotationIntrospector() {
return new JacksonAnnotationIntrospector() {
#Override
protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) {
if (annoClass == JsonTypeInfo.class && _hasAnnotation(annotated, FileJsonTypeInfo.class)) {
FileJsonTypeInfo fileJsonTypeInfo = _findAnnotation(annotated, TransferJsonTypeInfo.class);
if(fileJsonTypeInfo != null && fileJsonTypeInfo.jsonTypeInfo() != null) {
return (A) fileJsonTypeInfo.jsonTypeInfo(); // this cast should be safe because we have checked the annotation class
}
}
if (ignoreJsonAnnotations(annoClass)) return null;
return super._findAnnotation(annotated, annoClass);
}
};
}
private static <A extends Annotation> boolean ignoreJsonAnnotations(Class<A> annoClass) {
if (annoClass == JsonSerialize.class) {
return true;
}
if(annoClass == JsonDeserialize.class){
return true;
}
if(annoClass == JsonIdentityReference.class){
return true;
}
return annoClass == JsonIdentityInfo.class;
}
}
My custom annotation is defined and described like this:
/**
* This annotation inside of a annotation solution is a way to tell the importExportMapper how to serialize/deserialize
* objects that already have a wrongly defined #JsonTypeInfo annotation (wrongly defined for the importExportMapper).
*
* Idea is taken from here: https://stackoverflow.com/questions/58495480/how-to-properly-override-jacksonannotationintrospector-findannotation-to-replac
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface FileJsonTypeInfo {
JsonTypeInfo jsonTypeInfo();
}
And it is used like this:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonTypeInfo(defaultImpl = Customer.class, property = "", use = JsonTypeInfo.Id.NONE)
#TransferJsonTypeInfo(jsonTypeInfo = #JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "customeridentifier"))
#JsonIdentityReference(alwaysAsId = true)
#JsonDeserialize(using = CustomerClientDeserializer.class)
#JsonSerialize(using = CustomerClientSerializer.class)
private Customer customer;

Jackson's BeanDeserializerModifier does not work with final fields

I wrote a Custom Serializer and Custom Deserializer to serialize properties marked with #Confidential annotation.
#Data
public class Person {
private String name;
#Confidential
private String address;
}
The Custom Serializer serializes a POJO with following values:
{ "name": "John Doe", "address": "Kearney St"}
as follows:
{"name":"John Doe", "address": {"value":"IjIwMzEwIDU4dGggTG4gTkUi"}}
The Custom Deserializer is also able to deserialize the JSON back to the Person POJO fine.
However, when I make the fields in the Person POJO final, serialization continues to work, but deserialization fails.
#Data
public class Person {
private final String name;
#Confidential
private final String address;
}
Here's the BeanSerializerModifier implementation:
#AllArgsConstructor
public class CustomDeserializerModifier extends BeanDeserializerModifier {
private final ObjectMapper objectMapper;
#Override
public BeanDeserializerBuilder updateBuilder(final DeserializationConfig config,
final BeanDescription beanDesc,
final BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> beanPropertyIterator = builder.getProperties();
beanPropertyIterator.forEachRemaining(settableBeanProperty -> {
final Confidential annotation = settableBeanProperty.getAnnotation(Confidential.class);
if (encryptedProperty != null) {
JsonDeserializer<Object> current = settableBeanProperty.getValueDeserializer();
final SettableBeanProperty newSettableBeanProperty =
settableBeanProperty.withValueDeserializer(
new CustomDeserializer(annotation, current, objectMapper)
);
builder.addOrReplaceProperty(newSettableBeanProperty, true);
}
});
return builder;
}
}
I found that CustomDeserializer, never gets called when the Person POJO fields are final.
Here's the error message:
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: {"name":"John Doe","address":{"value":"IjIwMzEwIDU4dGggTG4gTkUi"}}; line: 1, column: 30] (through reference chain: com.custom.model.Person["address"])
Can a Jackson expert please tell me why my CustomDeserializer isn't getting invoked when the POJO fields are final.
Thank you!
As mentioned, serialization will perfectly work for both mutable and immutable fields. Deserialization issue will only occur when using immutable fields as the BeanDeserializerModifier won't work in such a case.
In the Jackson terminology, immutable fields are named creator properties, meaning they are initialized using creators. See BeanDeserializerBase#resolve.
To correctly handle this use case, the ObjectMapper may be created with a custom DeserializationContext (an extended implementation of the ObjectMapper may also set the protected field related to the deserialization context).
Then, through an override of the method DeserializationContext#handleSecondaryContextualization, it will be possible to change the deserialization to make it work.
There is maybe other possibilities but this one is working fine with encryption.

Custom deserialisation of JSON field using Jackson

I'm using Jackson to deserialize some JSON and I've run into some trouble while trying to use a custom deserializer for one of the fields.
class MyClass
{
private static class SpecialPropertyDeserializer extends JsonDeserializer<SpecialProperty>
{
#Override
public SpecialProperty deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException, JsonProcessingException
{
// do some custom deserialisation
}
}
private static class SpecialProperty
{
private String m_foo;
private String m_bar;
#JsonCreator
SpecialProperty(#JsonProperty("foo") String foo,
#JsonProperty("bar") String bar)
{
m_foo = foo;
m_bar = bar;
}
}
private String m_identifier;
private String m_version;
#JsonDeserialize(using = SpecialPropertyDeseializer.class)
private SpecialProperty m_specialProperty;
#JsonCreator
MyClass(#JsonProperty("identifier") String identifier,
#JsonProperty("version") String version,
#JsonProperty("specialProperty") SpecialProperty specialProperty)
{
m_identifier = identifier;
m_version = version;
m_specialProperty = specialProperty;
}
}
and this is the JSON I want to deserialize:
{
"identifier" : "some-id",
"version" : "1.7",
"specialProperty" : {
"foo" : "str1",
"bar" : "str2"
},
}
I invoke the mapper as follows:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
return objectMapper.readValue(input, MyClass.class);
I've observed the following behaviour:
Without a special property it all works fine - i.e. remove all
references to SpecialProperty from the code and the JSON.
If I include SpecialProperty in the JSON but remove the custom
deserializer for it then it also works fine. The ctor for
SpecialProperty is called.
With the custom deserializer it doesn't work. The ctor for SpecialProperty is called but the custom deserializer is not.
What am I doing wrong?
#JsonDeserialize annotation can be placed on a field, a setter or a class. Jackson will take it into account if what is annotated is what it uses to set the value.
E.g.1 It will notice #JsonDeserialize over a setter if it uses the setter to set the value of a field.
E.g.2 It will notice #JsonDeserialize over a field if it directly sets this field without using a setter or a constructor.
It will tend to take it into account if it's on a class unless it's overridden by a more specific annotation on a field or setter docs. I reckon the docs could be clearer on the above details.
In your case you have the annotation over the SpecialProperty field but you are setting this field in the MyClass constructor so it's ignored.
In this case you can move #JsonDeserialize over the class instead of over the field. That's probably the simplest solution in your case. E.g.
#JsonDeserialize(using = MyClass.SpecialPropertyDeserializer.class)
private static class SpecialProperty {
Or you can skip the annotation altogether and register the deserializer on the mapper. First make SpecialProperty and SpecialPropertyDeserializer non private in MyClass and then:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyClass.SpecialProperty.class, new MyClass.SpecialPropertyDeserializer());
objectMapper.registerModule(module);
You can also get rid of constructor of MyClass and the current annotation over the SpecialProperty field will be taken into account.

Jersey - JSON marshall only specific fields

My REST service returns following JSON
{
"name": "John",
"id" : 10
}
Can I use Jersey to marshall it into following Bean:
public class User{
private String name;
//getter & setter
}
I wanted to do this with following code but it doesn't work
WebResource webResource = client.resource(url);
webResource.accept(MediaType.APPLICATION_JSON_TYPE);
User user = webResource.get(User.class);
Is this even possible or I have to implement full JSON structure in Java Beans to get it work?
I know that I can parse this JSON with Jackson and any other methods.
With Jackson, easiest way is to configure ObjectMapper like so:
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
Check this sample provider
package com.company.rest.jersey;
#Provider
#Component
#Produces({MediaType.APPLICATION_JSON})
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
ObjectMapper mapper;
public JacksonMapperProvider(){
mapper = new ObjectMapper();
mapper.configure(Feature.INDENT_OUTPUT, true);
// Serialize dates using ISO8601 format
// Jackson uses timestamps by default, so use StdDateFormat to get ISO8601
mapper.getSerializationConfig().setDateFormat(new StdDateFormat());
// Deserialize dates using ISO8601 format
// MilliDateFormat simply adds milliseconds to string if missing so it will parse
mapper.getDeserializationConfig().setDateFormat(new MilliDateFormat());
// Prevent exceptions from being thrown for unknown properties
mapper.configure(
DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,false);
}
#Override
public ObjectMapper getContext(Class<?> aClass) {
return mapper;
}
}
With Jackson :
You have two options:
Jackson works on setters-getters of fields. So, you can just remove getter of field which you want to omit in JSON. ( If you don't need getter at other place.)
Or, you can use the #JsonIgnore annotation of Jackson on getter method of that field and you see there in no such key-value pair in resulted JSON.
#JsonIgnore
public int getSecurityCode(){
return securityCode;
}
In your bean, add the annotation #JsonIgnoreProperties(ignoreUnknown = true) at the class level and it should skip the id property in the JSON since it's not present in the bean.
#JsonIgnoreProperties(ignoreUnknown = true)
public class User{
private String name;
//getter & setter
}
(See http://wiki.fasterxml.com/JacksonAnnotations for details)

De-serializing JSON to polymorphic object model using Spring and JsonTypeInfo annotation

I have the following object model in my Spring MVC (v3.2.0.RELEASE) web application:
public class Order {
private Payment payment;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.WRAPPER_OBJECT)
#JsonSubTypes.Type(name = "creditCardPayment", value = CreditCardPayment.class)
public interface Payment {}
#JsonTypeName("creditCardPayment")
public class CreditCardPayment implements Payment {}
When I serialise the Order class to JSON, I get the following result (which is exactly what I want):
{
"payment" : {
"creditCardPayment": {
...
}
}
Unfortunately, if I take the above JSON and try to de-serialise it back into my object model, I get the following exception:
Could not read JSON: Could not resolve type id 'creditCardPayment'
into a subtype of [simple type, class Payment] at [Source:
org.apache.catalina.connector.CoyoteInputStream#19629355; line: 1,
column: 58] (through reference chain: Order["payment"]); nested
exception is com.fasterxml.jackson.databind.JsonMappingException:
Could not resolve type id 'creditCardPayment' into a subtype of
[simple type, class Payment] at [Source:
org.apache.catalina.connector.CoyoteInputStream#19629355; line: 1,
column: 58] (through reference chain: Order["payment"])
My application is configured via Spring JavaConf, as follows:
#Configuration
#EnableWebMvc
public class AppWebConf extends WebMvcConfigurerAdapter {
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(Include.NON_NULL);
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
return objectMapper;
}
#Bean
public MappingJackson2HttpMessageConverter mappingJacksonMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper());
return converter;
}
#Bean
public Jaxb2RootElementHttpMessageConverter jaxbMessageConverter() {
return new Jaxb2RootElementHttpMessageConverter();
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jaxbMessageConverter());
converters.add(mappingJacksonMessageConverter());
}
}
For testing, I have a controller with 2 methods, one returns an Order for HTTP GET request (this one works) and one that accepts an Order via a HTTP POST (this one fails), e.g.
#Controller
public class TestController {
#ResponseBody
#RequestMapping(value = "/test", method = RequestMethod.GET)
public Order getTest() {}
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void postTest(#RequestBody order) {}
}
I have tried all suggestions from the various discussions on SO but so far had no luck. Can anyone spot what I'm doing wrong?
Try to register subtype using ObjectMapper.registerSubtypes instead of using annotations
The method registerSubtypes() works!
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
public interface Geometry {
//...
}
public class Point implements Geometry{
//...
}
public class Polygon implements Geometry{
//...
}
public class LineString implements Geometry{
//...
}
GeoJson geojson= null;
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerSubtypes(Polygon.class,LineString.class,Point.class);
try {
geojson=mapper.readValue(source, GeoJson.class);
} catch (IOException e) {
e.printStackTrace();
}
Note1: We use the Interface and the implementing classes. I fyou want jackson to de-serialize the classes as per their implementing classes, you have to register all of them using ObjectMapper's "registerSubtypes" method.
Note2: In addition you use, " #JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")" as annotation with your Interface.
You can also define the order of properties when mapper writes a value of your POJO as a json.
This you can do using below annotation.
#JsonPropertyOrder({"type","crs","version","features"})
public class GeoJson {
private String type="FeatureCollection";
private List<Feature> features;
private String version="1.0.0";
private CRS crs = new CRS();
........
}
Hope this helps!
I had a similar issue while working on a dropwizard based service. I don't fully understand why things didn't work for me in the same way that the dropwizard code works, but I know why the code in the original post doesn't work. #JsonSubTypes wants an array of sub types, not a single value. So if you replace the line...
#JsonSubTypes.Type(name = "creditCardPayment", value = CreditCardPayment.class)
with...
#JsonSubTypes({ #JsonSubTypes.Type(name = "creditCardPayment", value = CreditCardPayment.class) })
I believe your code will work.
For those that are having this same error message pop up, you may be having an issue with the subtypes being discovered. Try adding a line like the one above or looking for issue with the discovery of the classes that have the #JsonTypeName tag in them.
Rashmin's answer worked, and I found an alternative way to avoid the com.fasterxml.jackson.databind.JsonMappingException: Could not resolve type id into a subtype of Blah issue without needing to use registerSubtypes. What you can do is add the following annotation to the parent class:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type")
Note that the difference is JsonTypeInfo.Id.CLASS instead of JsonTypeInfo.Id.NAME. The downside is that the created JSON will contain the entire class name including the full namespace. The upside is that you don't have to worry about registering subtypes.
In my case I had added defaultImpl = SomeClass.class to #JsonTypeInfo and was trying to convert it SomeClass2.class
Encountered the same error and used the equivalent of the below JSON (instead of CreditCardPayment used my class name) as the input for deserializer and it worked:
{
"type": "CreditCardPayment",
...
}
In my case it was an old version of jackson-databind. I solved with version 2.11.0

Categories

Resources