How to use Jackson BeanDeserializerModifier? - jackson

I am trying to implement a custom deserializer.
Because I only want to add functionality to the default deserializer, I tried to store in my custom deserializer the default one: I would like to use the default to deserialize the json and then add other information.
I am trying to use BeanDeserializerModifier to register the custom deserializer.
SimpleModule module = new SimpleModule("ModelModule", Version.unknownVersion());
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
JsonDeserializer<?> configuredDeserializer = super.modifyDeserializer(config, beanDesc, deserializer);
if (Document.class.isAssignableFrom(beanDesc.getBeanClass())) {
logger.debug("Returning custom deserializer for documents");
configuredDeserializer = new DocumentDeserializer(configuredDeserializer, (Class<Document>)beanDesc.getBeanClass());
}
return configuredDeserializer;
}
});
As you can see, if the object to generate is a "Document", I am modifying the deserializer returning a custom deserializer. I am passing the default deserializer to the constructor so I can use it later.
When I try to deserialize, Jackson fails with the error:
No _valueDeserializer assigned(..)
I have investigated and it seems that the default deserializer does not have the correct deserializers for its properties: for all the properties, it is using the deserializer FailingDeserializer that, of course, fails and returns the error mentioned above. This deserializer is supposed to be substituted but it is not.
It seems that, after calling the method modifyDeserializer, Jackson completes the configuration.
The custom deserializer that I am using is:
#SuppressWarnings("serial")
public class DocumentDeserializer extends StdDeserializer<Document> {
private JsonDeserializer<?> defaultDeserializer;
private DocumentDeserializer(JsonDeserializer<?> defaultDeserializer, Class<? extends Document> clazz) {
super(clazz);
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Document deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Document documentDeserialized = (Document) defaultDeserializer.deserialize(jp, ctxt);
/* I want to modify the documentDeserialized before returning it */
return documentDeserialized;
}
}
UPDATE:
I solved the problem using a different Deserializer:
public class CustomDeserializerModifier extends BeanDeserializerModifier {
private static final Logger logger = Logger.getLogger(CustomDeserializerModifier.class);
public CustomDeserializerModifier (Factory factory) {
this.factory = factory;
}
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
JsonDeserializer<?> configuredDeserializer;
if (CustomDeserializedNode.class.isAssignableFrom(beanDesc.getBeanClass())) {
Converter<Object, Object> conv = beanDesc.findDeserializationConverter();
JavaType delegateType = conv.getInputType(config.getTypeFactory());
configuredDeserializer = new CustomDeserializedNodeDeserializer(conv, delegateType, (JsonDeserializer<Document>) deserializer,
(Class<? extends CustomDocument<?>>)beanDesc.getBeanClass());
} else {
configuredDeserializer = super.modifyDeserializer(config, beanDesc, deserializer);
}
return configuredDeserializer;
}
#SuppressWarnings("serial")
public class CustomDeserializedNodeDeserializer extends StdDelegatingDeserializer<Object> {
private Class<? extends CustomDocument<?>> beanClass;
public CustomDeserializedNodeDeserializer(Converter<Object,Object> converter,
JavaType delegateType, JsonDeserializer<Document> delegateDeserializer, Class<? extends CustomDocument<?>> beanClass) {
super(converter, delegateType, delegateDeserializer);
this.beanClass = beanClass;
}
#Override
public CustomDeserializedNode deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
CustomDeserializedNode node = (CustomDeserializedNode)factory.createCustomDocument(beanClass);
CustomDeserializedNode documentDeserialized = (Document) super.deserialize(jp, ctxt, node);
return documentDeserialized;
}
}
}
Probably extending StdDelegatingDeserializer does what #StaxMan is suggesting.

This should be added in a FAQ, but what you need to do is to implement 2 interfaces:
ResolvableDeserializer (method resolve(...))
ContextualDeserializer (method createContextual(...))
and delegate these calls to defaultDeserializer in case it implements one or both interfaces. These are required for deserializer initialization; especially ContextualDeserializer through which property annotations are made available to deserializers.
And ResolvableDeserializer is used by BeanDeserializer to get deserializers for properties it has, if any; this is where _valueDeserializer in question is likely to be fetched.

Related

Access JAX-RS resource annotations from a JsonbSerializer

I have an application running on Payara 4 using a custom GSON JSON adapter. I would like to migrate to Payara 5 (5.191) and start using JSON-B. In our current application we can control the JSON output using annotations on a resource.
For example using #Summarize:
#GET
#Path("summary/{encryptedId}")
#Produces(MediaType.APPLICATION_JSON)
#Summarize
public Address findSummarized(#PathParam("encryptedId") String encryptedId) {
return super.find(encryptedId);
}
it will cause a different GSON configuration to be used in our #Provider:
#Provider
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class GsonProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
public GsonProvider() {
gson = getGson(EntityAdapter.class);
gsonSummary = getGson(EntitySummaryAdapter.class);
}
...
#Override
public void writeTo(T object,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream)
throws IOException, WebApplicationException {
boolean summarize = contains(annotations, Summarize.class);
try (PrintWriter printWriter = new PrintWriter(entityStream)) {
printWriter.write((summarize ? gsonSummary : gson).toJson(object));
printWriter.flush();
}
}
}
I want to do something similar in the new JSON-B setup. I annotated our entities with #JsonbTypeSerializer(MySerializer.class), so I would like to be able to detect from within the serializer what it should do: either create a full serialized JSON object, or a summary.
What I hoped to do is set a property in the JsonbConfig, like so:
JsonbConfig config = new JsonbConfig()
.setProperty("com.myCompany.jsonb.summarize", true);
and read it in the serializer using #Context (just guessing that this might work here), like so:
#Context
private JsonbConfiguration config;
.. but that's not. Is there any way to access JAX-RS resource annotations from a JsonbSerializer?
You could accomplish a similar goal using two separate Jsonb instances in your JAX-RS provider class like so:
#Provider
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class JsonbProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private static final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
.withAdapters(new EntityAdapter()));
private static final Jsonb jsonbSummary = JsonbBuilder.create(new JsonbConfig()
.withAdapters(new EntitySummaryAdapter()));
...
#Override
public void writeTo(T object,
Class<?> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream)
throws IOException, WebApplicationException {
boolean summarize = contains(annotations, Summarize.class);
try (PrintWriter printWriter = new PrintWriter(entityStream)) {
printWriter.write((summarize ? jsonbSummary : jsonb).toJson(object));
printWriter.flush();
}
}
}
In the end I opted to create summaries from within my entities and drop the annotation on my REST resources. It was a bit of work, but I think it has been worth it.
I created a Summarizable interface and added a default method there to create a simple map summary of any entity, based on a extended version of the PropertyVisibilityStrategy we created for the full version of the entities.
public interface Summarizable {
public default Map<String, Object> toSummary() {
SummaryPropertyVisibilityStrategy summaryStrategy = new SummaryPropertyVisibilityStrategy();
Map<String, Object> summary = new LinkedHashMap<>();
ReflectionUtils.getFields(this.getClass())
.stream()
.filter(summaryStrategy::isVisible)
.map(f -> new AbstractMap.SimpleEntry<>(f.getName(), summarize(f)))
.filter(e -> e.getValue() != null)
.forEach(e -> summary.put(e.getKey(), e.getValue()));
return summary;
}
public default Object summarize(final Field field) {
Object value = ReflectionUtils.getValueJsonb(this, field);
return value != null && Stream.of(ManyToOne.class, OneToOne.class).anyMatch(field::isAnnotationPresent)
? value.toString()
: value;
}
}
public static Object getValueJsonb(final Object object, final Field field) {
field.setAccessible(true);
JsonbTypeAdapter adapterAnnotation = field.getAnnotation(JsonbTypeAdapter.class);
try {
Object value = field.get(object);
return adapterAnnotation == null
? value
: adapterAnnotation.value().newInstance().adaptToJson(value);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}

hide Jackson fields based on costume dynamic criteria for JaxRS Respose

Idea is simple. I have a object and I would like to hide some fields based on the some specific roles.
I have roles in the system "dog", "cat" etc.
class Food{
String name;
#HideInfoForTheRoles({"dog", "cat"})
String age;
}
So I think to create something like that:
public String hideForRole(T object, String role){
// return new json
}
Or maybe I can override some denationalization method to force Jackson to hide field based on my annotation?
You could use #JsonView. That's probably the easiest solution, as #JsonView works out-of-the-box with JAX-RS.
Alternativerly, it could be achieved with a BeanPropertyFilter, similar to another solution I put together a while ago.
Start defining your annotation:
#Documented
#Retention(RUNTIME)
#Target({FIELD})
public #interface HiddenForRoles {
String[] value();
}
Then define your BeanPropertyFilter, which can extend SimpleBeanPropertyFilter:
public class HiddenForRolesPropertyFilter extends SimpleBeanPropertyFilter {
private String allowedRole;
public HiddenForRolesPropertyFilter(String allowedRole) {
this.allowedRole = allowedRole;
}
#Override
public void serializeAsField(Object pojo, JsonGenerator jgen,
SerializerProvider provider,
PropertyWriter writer) throws Exception {
HiddenForRoles hiddenForRoles = writer.getAnnotation(HiddenForRoles.class);
if (hiddenForRoles != null) {
if (Arrays.asList(hiddenForRoles.value()).contains(allowedRole)) {
writer.serializeAsOmittedField(pojo, jgen, provider);
return;
}
}
// If no annotation is provided, the property will be serialized
writer.serializeAsField(pojo, jgen, provider);
}
}
Place the #HiddenForRoles annotation in your fields, according to your needs and ensure the class is annotated with #JsonFilter:
#Data
#JsonFilter("hiddenForRolesPropertyFilter")
public class Foo {
private String bar;
#HiddenForRoles({"cat"})
private String biz;
}
Finally, register the filter in a ContextResolver for ObjectMapper:
String currentUserRole = // Get role from the current user
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("hiddenForRolesPropertyFilter",
new HiddenForRolesPropertyFilter(currentUserRole));
ObjectMapper mapper = new ObjectMapper();
mapper.setFilterProvider(filterProvider);
If you want to make your filter "global", that is, to be applied to all beans, you can create a mix-in class and annotate it with #JsonFilter:
#JsonFilter("hiddenForRolesPropertyFilter")
public class HiddenForRolesPropertyFilterMixIn {
}
Then bind the mix-in class to Object:
mapper.addMixIn(Object.class, HiddenForRolesPropertyFilterMixIn.class);
Create annotation that supports on FIELD and METHOD
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD, ElementType.METHOD})
public #interface HideFor{
String[] roles() default{};
}
and logic that supports annotation for both field and methods
public class AccessRestrictionFilter extends SimpleBeanPropertyFilter {
#Override
public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if(writer.getAnnotation(HideFor.class)!=null && isHidable( Arrays.asList(writer.getAnnotation(HideFor.class).roles()))){
logger.debug("Found restriction on the getter method of the field: " + pojo + " Restriction For" + Arrays.toString(writer.getAnnotation(HideFor.class).roles()) );
return;
}
Field[] fields = jgen.getCurrentValue().getClass().getDeclaredFields();
Optional<Field> field = Arrays.stream(fields)
.filter(f-> f.getName().equalsIgnoreCase(writer.getName())).findAny();
if(field.isPresent() && field.get().getAnnotation(HideFor.class)!=null){
if(isHidable( Arrays.asList(writer.getAnnotation(HideFor.class).roles()))){
System.out.println("Found restriction on the field " + field.get().getName() + " Restriction For " + Arrays.toString(writer.getAnnotation(HideFor.class).roles()));
return;
}
}
writer.serializeAsField(pojo, jgen, provider);
}
private boolean isHidable(List<String> rolesToHide){ // imlement the logic // }
}
Usage:
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("AccessRestrictionFilter", new AccessRestrictionFilter());
new ObjectMapper().writer(filterProvider ).writeValueAsString(myObjToFilter);
I use Jersey/Spring and my configuration looks like this:
#Provider
#Produces({MediaType.APPLICATION_JSON})
public class JacksonJsonProvider extends JacksonJaxbJsonProvider {
public JacksonJsonProvider(AccessRestrictionFilter filter) {
ObjectMapper objectMapper = new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.setFilterProvider(new SimpleFilterProvider().addFilter("AccessRestriction", filter));
setMapper(objectMapper);
}
}
And Filter:
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
#Bean("accessRestrictionFilter")
public AccessRestrictionFilter accessRestrictionFilter(){
return new AccessRestrictionFilter();
}
Note: in the filter I use the Security Context, because of this scope of the filter is Session (Not to share the state but create new object for each user)
and that's my POJO:
#JsonFilter("AccessRestrictionFilter")
public class MyClass {
#HideFor(roles = {"ROLE_USER", "ROLE_EDITOR"})
private int val;

java.lang.NullPointerException: null on AutoWiring a bean in StandAlone App

When trying to use #AutoWire feature with one of StandAlone Application unable to do so instead getting Null Pointer Exception. Please highlight my mistakes if any. Your help is appreciated.
Spring Ver 5.1.5.RELEASE and we're not using any xml config file to tell spring there are annotated classes to look into instead using #ComponentScan or #EnableAutoConfiguration at the top of AppConfig and boost strap the Context from main() class as a first line. But Autowiring works perfectly with internal bean/java classes of jdk(Environment) but not with custom POJO classes. If we're trying to get through getBean method then it works. But I'm trying to avoid creating context everywhere and using getBean() Please Refer below and help me only with your valuable guidelines.
public class ContextMaster {
private static AnnotationConfigApplicationContext appContext;
public static AnnotationConfigApplicationContext getApplicationContext() {
if (appContext == null) {
appContext = new AnnotationConfigApplicationContext(ContextConfig.class);
//appContext = new AnnotationConfigApplicationContext("com.xx.xx.xxx","xx.xxx.xxxx.xxx.datamanager");
logger.debug("Context Invoked !!");
}
return appContext;
}
}
#Configuration
#EnableAutoConfiguration
#PropertySource("classpath:db.properties")
#EnableTransactionManagement
#ComponentScans(value = {
#ComponentScan(basePackages = "xxxxx.datamanager"),
#ComponentScan(basePackages = "com.xx.xx.xxx"),
#ComponentScan(basePackages = "com.xx.xx.xxx.utils")})
public class AppConfig {
#Autowired
private Environment env;
#Bean
public DataSource getDataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(env.getProperty("db.driver"));
dataSource.setUrl(env.getProperty("db.url"));
return dataSource;
}
#Bean
public LocalSessionFactoryBean getSessionFactory() {
LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
//LocalSessionFactoryBean sessionFactoryBean = new AnnotationSessionFactoryBean();
factoryBean.setDataSource(getDataSource());
Properties props=new Properties();
props.put("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
props.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
props.put("hibernate.cache.region.factory_class", env.getProperty("hibernate.cache.region.factory_class"));
factoryBean.setHibernateProperties(props);
factoryBean.setAnnotatedClasses(xx.class, xxxx.class, xxxx.class, xxx.class);
return factoryBean;
}
#Bean
public HibernateTransactionManager getTransactionManager() {
return transactionManager;
}
}
// Here is NPE thrown when tried with auto-configured bean
#Component
public class Good extends Good11 {
#Autowired
private RxxxDyyyyHelper rdh;
//RxxxDyyyyHelper rdh =
ContextHelper.getApplicationContext().getBean(RxxxDyyyyHelper .class);
rdh.setProperty(); // NPE here
rdh.getProperty(); // NPE
}
// Here we're trying to initiate the LosUtils class
public class LosUtils {
public static void main(String args[]) throws Exception {
AnnotationConfigApplicationContext applicationContext = `ContextHelper.getApplicationContext();`
}
It seems like you didn't put the full code here, because your Good class won't compile this way..

camel custom marshalling with dataFormat name in header

I'm having two routes in two separated projects :
First route is setting the header with a data format bean name as a constant :
setHeader("dataFormatBeanName", constant("myFirstList"))
First route :
public class MyTest {
#Configuration
public static class MyTestConfig extends CamelConfiguration {
#Bean(name = "myFirstList")
public DataFormat getMyFirstListDataFormat() {
return new MyFirstListDataFormat();
}
#Bean(name = "mySecondList")
public DataFormat getMySecondListDataFormat() {
return new MySecondListDataFormat();
}
#Bean
public RouteBuilder route() {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
from("direct:testFirstDataFormat").setHeader("dataFormatBeanName", constant("myFirstList")).to("direct:myRoute");
from("direct:testSecondDataFormat").setHeader("dataFormatBeanName", constant("mySecondList")).to("direct:myRoute");
}
};
}
}
}
Second route is supposed to retrieve the bean name from the header and use it as a custom marshaller. Something like :
custom(header("dataFormatBeanName"))
(doesn't compile)
Anyone knows how I'm supposed to get my bean name from the header to use it in the custom method ?
#Component
public class MyRouteBuilder extends RouteBuilder {
#Override
public void configure() throws Exception {
final RouteDefinition routedefinition = this.from("direct:myRoute");
routedefinition.marshal().custom(??????????).to("netty4:tcp://{{route.address}}:{{port}}?textline=true&sync=true");
}
After a few more hours searching, here is the solution a found :
No changes in the first class.
Second class uses an anonymous DataFormat in which I retrieve the bean name from the header and get the spring bean from camel context before calling its marshal method.
The AbstractXxxDataFormat class belongs to project2 and is inherited by the Project1 DataFormat.
#Override
public void configure() throws Exception {
final RouteDefinition routedefinition = this.from("direct:myRoute");
routedefinition.marshal(new DataFormat() {
#Override
public void marshal(final Exchange exchange, final Object graph, final OutputStream stream) throws Exception {
AbstractXxxDataFormat myDataFormat = (AbstractGoalDataFormat) getContext().getRegistry().lookupByName(exchange.getIn().getHeader("dataFormatBeanName", String.class));
myDataFormat.marshal(exchange, graph, stream);
}
#Override
public Object unmarshal(final Exchange exchange, final InputStream stream) throws Exception {
return null;
}
});
routedefinition.to("netty4:tcp://{{route.address}}:{{port}}?textline=true&sync=true");
}
If there's any better solution available, I'll be interested.
Have you tried simple("${header.dataFormatBeanName}") to access the header?
Also, rather than passing the format bean name in a header in the first place, why not factor out each .marshal() call into two subroutes (one for formatBeanA and one for formatBeanB) and then call the appropriate subroute rather than setting the header in the first place? I believe this could be a cleaner approach.
If you really need to get it in the route as a variable (as opposed to a predicate to be used in the builder api) you could use an inline processor to extract it:
public class MyRouteBuilder extends RouteBuilder {
public void configure() throws Exception {
from("someEndpoint")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
String beanName = exchange.getHeader("beanNameHeader");
}
});
}
}
Just be careful of scope and concurrency when storing the extracted beanName however.
A collegue of mine (thanks to him) found the definite solution :
set bean name in the exchange properties :
exchange.setProperty("myDataFormat", "myDataFormatAutowiredBean");
retrieve the dataFormat bean with RecipientList pattern and (un)marshal :
routedefinition.recipientList(simple("dataformat:${property.myDataFormat}:marshal"));
routedefinition.recipientList(simple("dataformat:${property.myDataFormat}:unmarshal"));
Very concise and works just fine.

Jackson custom serializer

I try to use a custom Jackson serializer, but unfortunately the serializer will not be triggered.
In my Application I added Jackson:
JacksonJaxbJsonProvider jacksonProvider = new JacksonJaxbJsonProvider();
jacksonProvider.setMapper( mapper );
s.add( jacksonProvider );
return s;
I added a serializer for my class TestType:
#JsonSerialize(using = TestSerializer.class)
public class TestType {
private String test;
public String getTest(){
if (test==null || test.isEmpty()){
test="test";
}
return test;
}
public void setTest(String test) {
this.test = test;
}
}
public class TestSerializer extends JsonSerializer<TestType> {
#Override
public void serialize(TestType value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("special serialization", value.getTest());
jgen.writeEndObject();
}
}
The API:
#GET
#Produces({"application/json; charset=UTF-8"})
public Response getTest(#HeaderParam( RestHelper.HEADER_PARAM_ORIGIN ) String origin, #Context HttpServletRequest request, #Context HttpHeaders headers) {
TestType test = new TestType();
test.setTest("test");
return Response.status(200).entity( test ).build();
}
Did I miss any step to register the serializer?
Regards,
hyperion
That looks ok. Just makes sure you are using Jackson 2.x annotations (com.fasterxml.jackson) with Jackson 2.x ObjectMapper. Since annotation names of 1.x are the same (but Java package differs), they will not work, despite looking identical (only import statement in sources differing).
Note, too, that you can implement JsonSerializable which would remove the need to use #JsonSerialize annotation here.