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;
Related
I was trying to filter out certain fields from serialization via SimpleBeanPropertyFilter using the following (simplified) code:
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
SimpleFilterProvider filterProvider = new SimpleFilterProvider().addFilter("test",
SimpleBeanPropertyFilter.filterOutAllExcept("data1"));
try {
String json = mapper.writer(filterProvider).writeValueAsString(new Data());
System.out.println(json); // output: {"data1":"value1","data2":"value2"}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
private static class Data {
public String data1 = "value1";
public String data2 = "value2";
}
Us I use SimpleBeanPropertyFilter.filterOutAllExcept("data1")); I was expecting that the created serialized Json string contains only {"data1":"value1"}, however I get {"data1":"value1","data2":"value2"}.
How to create a temporary writer that respects the specified filter (the ObjectMapper can not be re-configured in my case).
Note: Because of the usage scenario in my application I can only accept answers that do not use Jackson annotations.
If for some reason MixIns does not suit you. You can try this approach:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector(){
#Override
public boolean hasIgnoreMarker(final AnnotatedMember m) {
List<String> exclusions = Arrays.asList("field1", "field2");
return exclusions.contains(m.getName())|| super.hasIgnoreMarker(m);
}
});
You would normally annotate your Data class to have the filter applied:
#JsonFilter("test")
class Data {
You have specified that you can't use annotations on the class. You could use mix-ins to avoid annotating Data class.
#JsonFilter("test")
class DataMixIn {}
Mixins have to be specified on an ObjectMapper and you specify you don't want to reconfigure that. In such a case, you can always copy the ObjectMapper with its configuration and then modify the configuration of the copy. That will not affect the original ObjectMapper used elsewhere in your code. E.g.
ObjectMapper myMapper = mapper.copy();
myMapper.addMixIn(Data.class, DataMixIn.class);
And then write with the new ObjectMapper
String json = myMapper.writer(filterProvider).writeValueAsString(new Data());
System.out.println(json); // output: {"data1":"value1"}
The example of excluding properties by name:
public Class User {
private String name = "abc";
private Integer age = 1;
//getters
}
#JsonFilter("dynamicFilter")
public class DynamicMixIn {
}
User user = new User();
String[] propertiesToExclude = {"name"};
ObjectMapper mapper = new ObjectMapper()
.addMixIn(Object.class, DynamicMixIn.class);
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("dynamicFilter", SimpleBeanPropertyFilter.filterOutAllExcept(propertiesToExclude));
mapper.setFilterProvider(filterProvider);
mapper.writeValueAsString(user); // {"name":"abc"}
You can instead of DynamicMixIn create MixInByPropName
#JsonIgnoreProperties(value = {"age"})
public class MixInByPropName {
}
ObjectMapper mapper = new ObjectMapper()
.addMixIn(Object.class, MixInByPropName.class);
mapper.writeValueAsString(user); // {"name":"abc"}
Note: If you want exclude property only for User you can change parameter Object.class of method addMixIn to User.class
Excluding properties by type you can create MixInByType
#JsonIgnoreType
public class MixInByType {
}
ObjectMapper mapper = new ObjectMapper()
.addMixIn(Integer.class, MixInByType.class);
mapper.writeValueAsString(user); // {"name":"abc"}
It seems you have to add an annotation which indicts which filter to use when doing the serialization to the bean class if you want the filter to work:
#JsonFilter("test")
public class Data {
public String data1 = "value1";
public String data2 = "value2";
}
EDIT
The OP has just added a note that just take the answer that not using a bean animation, then if the field you want to export is very less amount, you can just retrieve that data and build a Map of List yourself, there seems no other way to do that.
Map<String, Object> map = new HashMap<String, Object>();
map.put("data1", obj.getData1());
...
// do the serilization on the map object just created.
If you want to exclude specific field and kept the most field, maybe you could do that with reflect. Following is a method I have written to transfer a bean to a map you could change the code to meet your own needs:
protected Map<String, Object> transBean2Map(Object beanObj){
if(beanObj == null){
return null;
}
Map<String, Object> map = new HashMap<String, Object>();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(beanObj.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor property : propertyDescriptors) {
String key = property.getName();
if (!key.equals("class")
&& !key.endsWith("Entity")
&& !key.endsWith("Entities")
&& !key.endsWith("LazyInitializer")
&& !key.equals("handler")) {
Method getter = property.getReadMethod();
if(key.endsWith("List")){
Annotation[] annotations = getter.getAnnotations();
for(Annotation annotation : annotations){
if(annotation instanceof javax.persistence.OneToMany){
if(((javax.persistence.OneToMany)annotation).fetch().equals(FetchType.EAGER)){
List entityList = (List) getter.invoke(beanObj);
List<Map<String, Object>> dataList = new ArrayList<>();
for(Object childEntity: entityList){
dataList.add(transBean2Map(childEntity));
}
map.put(key,dataList);
}
}
}
continue;
}
Object value = getter.invoke(beanObj);
map.put(key, value);
}
}
} catch (Exception e) {
Logger.getAnonymousLogger().log(Level.SEVERE,"transBean2Map Error " + e);
}
return map;
}
But I recommend you to use Google Gson as the JSON deserializer/serializer And the main reason is I hate dealing with exception stuff, it just messed up with the coding style.
And it's pretty easy to satisfy your need with taking advantage of the version control annotation on the bean class like this:
#Since(GifMiaoMacro.GSON_SENSITIVE) //mark the field as sensitive data and will not export to JSON
private boolean firstFrameStored; // won't export this field to JSON.
You can define the Macro whether to export or hide the field like this:
public static final double GSON_SENSITIVE = 2.0f;
public static final double GSON_INSENSITIVE = 1.0f;
By default, Gson will export all field that not annotated by #Since So you don't have to do anything if you do not care about the field and it just exports the field.
And if some field you are not want to export to json, ie sensitive info just add an annotation to the field. And generate json string with this:
private static Gson gsonInsensitive = new GsonBuilder()
.registerTypeAdapter(ObjectId.class,new ObjectIdSerializer()) // you can omit this line and the following line if you are not using mongodb
.registerTypeAdapter(ObjectId.class, new ObjectIdDeserializer()) //you can omit this
.setVersion(GifMiaoMacro.GSON_INSENSITIVE)
.disableHtmlEscaping()
.create();
public static String toInsensitiveJson(Object o){
return gsonInsensitive.toJson(o);
}
Then just use this:
String jsonStr = StringUtils.toInsensitiveJson(yourObj);
Since Gson is stateless, it's fine to use a static method to do your job, I have tried a lot of JSON serialize/deserialize framework with Java, but found Gson to be the sharp one both performance and handily.
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);
I need to add a custom Jackson deserializer for java.lang.String to my Spring 4.1.x MVC application. However all answers (such as this) refer to configuring the ObjectMapper for the complete web application and the changes will apply to all Strings across all #RequestBody in all controllers.
I only want to apply the custom deserialization to #RequestBody arguments used within particular controllers. Note that I don't have the option of using #JsonDeserialize annotations for the specific String fields.
Can you configure custom deserialization for specific controllers only?
To have different deserialization configurations you must have different ObjectMapper instances but out of the box Spring uses MappingJackson2HttpMessageConverter which is designed to use only one instance.
I see at least two options here:
Move away from MessageConverter to an ArgumentResolver
Create a #CustomRequestBody annotation, and an argument resolver:
public class CustomRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private final ObjectMapperResolver objectMapperResolver;
public CustomRequestBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
this.objectMapperResolver = objectMapperResolver;
}
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CustomRequestBody.class) != null;
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
ObjectMapper objectMapper = objectMapperResolver.getObjectMapper();
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
return objectMapper.readValue(request.getInputStream(), methodParameter.getParameterType());
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}
#CustomRequestBody annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CustomRequestBody {
boolean required() default true;
}
ObjectMapperResolver is an interface we will be using to resolve actual ObjectMapper instance to use, I will discuss it below. Of course if you have only one use case where you need custom mapping you can simply initialize your mapper here.
You can add custom argument resolver with this configuration:
#Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public CustomRequestBodyArgumentResolver customBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
return new CustomRequestBodyArgumentResolver(objectMapperResolver)
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(customBodyArgumentResolver(objectMapperResolver()));
}
}
Note: Do not combine #CustomRequestBody with #RequestBody, it will be ignored.
Wrap ObjectMapper in a proxy that hides multiple instances
MappingJackson2HttpMessageConverter is designed to work with only one instance of ObjectMapper. We can make that instance a proxy delegate. This will make working with multiple mappers transparent.
First of all we need an interceptor that will translate all method invocations to an underlying object.
public abstract class ObjectMapperInterceptor implements MethodInterceptor {
#Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return ReflectionUtils.invokeMethod(invocation.getMethod(), getObject(), invocation.getArguments());
}
protected abstract ObjectMapper getObject();
}
Now our ObjectMapper proxy bean will look like this:
#Bean
public ObjectMapper objectMapper(ObjectMapperResolver objectMapperResolver) {
ProxyFactory factory = new ProxyFactory();
factory.setTargetClass(ObjectMapper.class);
factory.addAdvice(new ObjectMapperInterceptor() {
#Override
protected ObjectMapper getObject() {
return objectMapperResolver.getObjectMapper();
}
});
return (ObjectMapper) factory.getProxy();
}
Note: I had class loading issues with this proxy on Wildfly, due to its modular class loading, so I had to extend ObjectMapper (without changing anything) just so I can use class from my module.
It all tied up together using this configuration:
#Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(objectMapper(objectMapperResolver()));
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jackson2HttpMessageConverter());
}
}
ObjectMapperResolver implementations
Final piece is the logic that determines which mapper should be used, it will be contained in ObjectMapperResolver interface. It contains only one look up method:
public interface ObjectMapperResolver {
ObjectMapper getObjectMapper();
}
If you do not have a lot of use cases with custom mappers you can simply make a map of preconfigured instances with ReqeustMatchers as keys. Something like this:
public class RequestMatcherObjectMapperResolver implements ObjectMapperResolver {
private final ObjectMapper defaultMapper;
private final Map<RequestMatcher, ObjectMapper> mapping = new HashMap<>();
public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper, Map<RequestMatcher, ObjectMapper> mapping) {
this.defaultMapper = defaultMapper;
this.mapping.putAll(mapping);
}
public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper) {
this.defaultMapper = defaultMapper;
}
#Override
public ObjectMapper getObjectMapper() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
for (Map.Entry<RequestMatcher, ObjectMapper> entry : mapping.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return defaultMapper;
}
}
You can also use a request scoped ObjectMapper and then configure it on a per-request basis. Use this configuration:
#Bean
public ObjectMapperResolver objectMapperResolver() {
return new ObjectMapperResolver() {
#Override
public ObjectMapper getObjectMapper() {
return requestScopedObjectMapper();
}
};
}
#Bean
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public ObjectMapper requestScopedObjectMapper() {
return new ObjectMapper();
}
This is best suited for custom response serialization, since you can configure it right in the controller method. For custom deserialization you must also use Filter/HandlerInterceptor/ControllerAdvice to configure active mapper for current request before the controller method is triggered.
You can create interface, similar to ObjectMapperResolver:
public interface ObjectMapperConfigurer {
void configureObjectMapper(ObjectMapper objectMapper);
}
Then make a map of this instances with RequstMatchers as keys and put it in a Filter/HandlerInterceptor/ControllerAdvice similar to RequestMatcherObjectMapperResolver.
P.S. If you want to explore dynamic ObjectMapper configuration a bit further I can suggest my old answer here. It describes how you can make dynamic #JsonFilters at run time. It also contains my older approach with extended MappingJackson2HttpMessageConverter that I suggested in comments.
Probably this would help, but it ain't pretty. It would require AOP. Also I did not validate it.
Create a #CustomAnnotation.
Update your controller:
void someEndpoint(#RequestBody #CustomAnnotation SomeEntity someEntity);
Then implemment the AOP part:
#Around("execution(* *(#CustomAnnotation (*)))")
public void advice(ProceedingJoinPoint proceedingJoinPoint) {
// Here you would add custom ObjectMapper, I don't know another way around it
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String body = request .getReader().lines().collect(Collectors.joining(System.lineSeparator()));
SomeEntity someEntity = /* deserialize */;
// This could be cleaner, cause the method can accept multiple parameters
proceedingJoinPoint.proceed(new Object[] {someEntity});
}
You can create custom deserializer for your String data.
Custom Deserializer
public class CustomStringDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String str = p.getText();
//return processed String
}
}
Now suppose the String is present inside a POJO use #JsonDeserialize annotation above the variable:
public class SamplePOJO{
#JsonDeserialize(using=CustomStringDeserializer.class)
private String str;
//getter and setter
}
Now when you return it as a response it will be Deserialized in the way you have done it in CustomDeserializer.
Hope it helps.
You could try Message Converters.
They have a context about http input request (for example, docs see here, JSON). How to customize you could see here.
Idea that you could check HttpInputMessage with special URIs, which used in your controllers and convert string as you want.
You could create special annotation for this, scan packages and do it automatically.
Note
Likely, you don't need implementation of ObjectMappers. You can use simple default ObjectMapper to parse String and then convert string as you wish.
In that case you would create RequestBody once.
You can define a POJO for each different type of request parameter that you would like to deserialize. Then, the following code will pull in the values from the JSON into the object that you define, assuming that the names of the fields in your POJO match with the names of the field in the JSON request.
ObjectMapper mapper = new ObjectMapper();
YourPojo requestParams = null;
try {
requestParams = mapper.readValue(JsonBody, YourPOJO.class);
} catch (IOException e) {
throw new IOException(e);
}
I need to provide custom deserialization of the Map and then each Property object has to be serialized by default serializer. This map is part of another object:
class PropertiesHolder {
Map<String, Property> properties;
}
I've defined mixin for the PropertiesHolder class:
class PropertiesHolderMixIn {
#JsonSerialize(using=PropertiesSerializer.class)
#JsonDeserialize(using=PropertiesDeserializer.class)
Map<String, Property> properties;
}
I have also mixin for Property class. The ObjectMapper initialization:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setMixInAnnotation(Property.class, PropertyMixIn.class);
module.setMixInAnnotation(PropertiesHolder.class, PropertiesHolderMixIn.class);
mapper.registerModule(module);
My deserializer:
class PropertiesDeserializer extends JsonDeserializer<Map<String, Property>> {
public Map<String, Property> deserialize(JsonParser jp, DeserializationContext ctxt) throws ... {
ArrayNode node = (ArrayNode) jp.readValueAsTree();
for (int i = 0, size = node.size() ; i < size ; i++) {
ObjectNode jn = (ObjectNode) node.get(i);
String key = jn.get("propertyName").textValue();
String value = jn.get("propertyValue").toString();
... HERE I need to call registered deserializer for Property class over value ...
}
}
}
I've looked at How do I call the default deserializer from a custom deserializer in Jackson, but it doesn't work form me ... it ends with NPE. Also the solution described in the post creates deserializer for the outer class which for me is defined as mixin and I don't want to create deserializer for this class.
Please, point me to a solution. Where can I get default deserializer for the Property object?
Thanks
The problem is that you will need a fully constructed default deserializer; and this requires that one gets built, and then your deserializer gets access to it. DeserializationContext is not something you should either create or change; it will be provided by ObjectMapper
So all you need to write in the deserialize() method is:
ObjectMapper mapper = (ObjectMapper)jp.getCodec();
Property property = mapper.readValue(jn.get("propertyValue").toString(), Property.class));
Solution is this line of code:
ObjectMapper mapper = (ObjectMapper)jp.getCodec();
Call this method within "deserialize(...)" method. So the important (for me) code fragment is:
ObjectMapper mapper = (ObjectMapper)jp.getCodec();
Property property = mapper.readValue(jn.get("propertyValue").toString(), Property.class));
Found on this blog.
With Jackson, it's easy to disable all annotations for a given ObjectMapper.
Is there a way to only disable one given annotation?
// disable all
ObjectMapper mapper = new ObjectMapper()
mapper.disable(MapperFeature.USE_ANNOTATIONS);
// disable one?
ObjectMapper mapper = new ObjectMapper()
mapper.disable(#JsonIgnore);
Using #JacksonAnnotationsInside, I've defined a custom Jackson annotation and I only want it to be used in certain circumstances.
This the best I've come across. I think I saw it on the Jackson user group forums somewhere.
Essentially it makes a custom annotation introspector, which returns null if it sees that it has a specific annotation (in this case JsonTypeInfo)
JacksonAnnotationIntrospector ignoreJsonTypeInfoIntrospector = new JacksonAnnotationIntrospector() {
#Override
protected TypeResolverBuilder<?> _findTypeResolver(
MapperConfig<?> config, Annotated ann, JavaType baseType) {
if (!ann.hasAnnotation(JsonTypeInfo.class)) {
return super._findTypeResolver(config, ann, baseType);
}
return null;
}
};
mapper.setAnnotationIntrospector(ignoreJsonTypeInfoIntrospector);
I think it's a better idea to override findPropertiesToIgnore method like this:
JacksonAnnotationIntrospector ignoreJsonTypeInfoIntrospector = new JacksonAnnotationIntrospector() {
#Override
public String[] findPropertiesToIgnore(AnnotatedClass ac) {
ArrayList<String> ret = new ArrayList<String>();
for (Method m : ac.getRawType().getMethods()) {
if(ReflectionUtils.isGetter(m)){
if(m.getAnnotation(Transient.class) != null)
ret.add(ReflectionUtils.getPropertyName(m));
}
};
return ret.toArray(new String[]{});
}
};
objectMapper = new ObjectMapper();
objectMapper.setAnnotationIntrospector(ignoreJsonTypeInfoIntrospector);
This solution worked for me. Check this for more info
private static final JacksonAnnotationIntrospector IGNORE_ENUM_ALIAS_ANNOTATIONS = new JacksonAnnotationIntrospector() {
#Override
protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) {
if (!annotated.hasAnnotation(JsonEnumAliasSerializer.class)) {
return super._findAnnotation(annotated, annoClass);
}
return null;
}
};
And my custom annotation:
#Retention(RetentionPolicy.RUNTIME)
#JacksonAnnotationsInside
#JsonSerialize(using = JsonEnumSerializer.class)
public #interface JsonEnumAliasSerializer {
}
And ObjectMapper:
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setAnnotationIntrospector(IGNORE_ENUM_ALIAS_ANNOTATIONS);
Here is a simpler way (August 2022) to do it in Kotlin (you can translate to Java if you really want to):
(I use this in a Ktor/KMongo project to prevent Transient attributes from being persisted because KMongo requires #JsonIgnore, which then also prevents my other mappers from serializing those properties!)
#Target(PROPERTY_GETTER)
annotation class Transient
private val TransientIntrospector: JacksonAnnotationIntrospector = object : JacksonAnnotationIntrospector() {
override fun hasIgnoreMarker(m: AnnotatedMember): Boolean =
m.allAnnotations.has(Transient::class.java) || super.hasIgnoreMarker(m)
}
private class IgnoreTransientModule : SimpleModule() {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.appendAnnotationIntrospector(TransientIntrospector)
}
}
// Just register the module in your `ObjectMapper` instance:
val ignoreTransientMapper = with(ObjectMapper()) {
registerModule(IgnoreTransientModule())
}
// And here is how to use it:
data class Customer(
val id: String? = null,
val firstName: String,
val lastName: String,
) {
#get:Transient
val fullName: String
get() = "$firstName $lastName"
}