How to properly override JacksonAnnotationIntrospector._findAnnotation to replace an annotation of the element

Viewed 892

I am trying to create some classes serializable by Jackson. I want to annotate some elements with standard Jackson annotation (let's consider JsonIgnore for this example) but I want them to have effect only in my specific mapper.

So I decided to create my own annotations like standard ones (e. g. MyJsonIgnore) and process them only in an annotation introspector used by my mapper. I've found overridable method _findAnnotation. Javadoc says the following:

...overridable that sub-classes may, if they choose to, 
mangle actual access block access ("hide" annotations) 
or perhaps change it.

I've found a way to block some annotations (however it involves overriding _hasAnnotation, not _findAnnotation) but I am completely stuck with changing annotations.

Here is some minimal example of what I am trying to do:

object Mappers {
    /**
     * Same as JsonIgnore but mapper-specific
     */
    annotation class MyJsonIgnore(val value: Boolean = true)

    private class MyIntrospector : JacksonAnnotationIntrospector() {
        override fun <A : Annotation> _findAnnotation(
            annotated: Annotated,
            annoClass: Class<A>
        ): A {
            if (annoClass == JsonIgnore::class.java && _hasAnnotation(annotated, MyJsonIgnore::class.java)) {
                val mji = _findAnnotation(annotated, MyJsonIgnore::class.java)
                return JsonIgnore(mji.value) // Does not compile, type mismatch
                // return JsonIgnore(mji.value) as A // Does not compile, annotation class cannot be instantiated, same as Java, see below
            }
            return super._findAnnotation(annotated, annoClass)
        }
    }

    fun myMapper(): ObjectMapper {
        return ObjectMapper().setAnnotationIntrospector(MyIntrospector())
    }
}

I also cannot do it with Java:

public class Mappers {
    /**
     * Same as JsonIgnore but mapper-specific
     */
    public @interface MyJsonIgnore {
        boolean value() default true;
    }

    private static class MyIntrospector extends JacksonAnnotationIntrospector {
        @Override
        protected <A extends Annotation> A _findAnnotation(Annotated annotated,
                                                           Class<A> annoClass) {
            if (annoClass == JsonIgnore.class && _hasAnnotation(annotated, MyJsonIgnore.class)) {
                MyJsonIgnore mji = _findAnnotation(annotated, MyJsonIgnore.class);
                return new JsonIgnore(mji.value()); // Does not compile, JsonIgnore is abstract
            }
            return super._findAnnotation(annotated, annoClass);
        }
    }

    static ObjectMapper myMapper() {
        return new ObjectMapper().setAnnotationIntrospector(new MyIntrospector())
    }
}

So what is the supposed way to change annotations by overriding this method? Is there any? Is my approach right or should I do it other way?

2 Answers

The main problem here is that you can't instantiate the annotation class. There is one solution though: you could store one annotation as a member of another annotation like this:

@Retention(AnnotationRetention.RUNTIME) // don't forget 
@Target(AnnotationTarget.FIELD)         // these annotations
annotation class MyJsonIgnore(val value: Boolean = true, val jsonIgnore: JsonIgnore = JsonIgnore())

So MyJsonIgnore will have an instantiated JsonIgnore inside. And then you can use it in your AnnotationIntrospector:

private class MyIntrospector : JacksonAnnotationIntrospector() {
    override fun <A : Annotation> _findAnnotation(
            annotated: Annotated,
            annoClass: Class<A>
    ): A? {
        if (annoClass == JsonIgnore::class.java && _hasAnnotation(annotated, MyJsonIgnore::class.java)) {
            val mji = _findAnnotation(annotated, MyJsonIgnore::class.java)
            if (mji?.value == true) {
                return mji.jsonIgnore as A // this cast should be safe because we have checked
                                           // the annotation class
            }
        }
        return super._findAnnotation(annotated, annoClass)
    }
}

I've tested this with the following class

class Test {
    @MyJsonIgnore
    val ignoreMe = "IGNORE"
    val field = "NOT IGNORE"
}

and method

fun main() {
    println(Mappers.myMapper().writeValueAsString(Test()))
    println(ObjectMapper().writeValueAsString(Test()))
}

and the output was

{"field":"NOT IGNORE"}
{"ignoreMe":"IGNORE","field":"NOT IGNORE"}

So here are my further thoughts. Kirill Simonov's answer is right and typesafe (an alternative would be to create an annotation instance using Kotlin reflection but it is not typesafe).

Here are some problems with my original code and thoughts about the original approach:

  1. You should consistently override _hasAnnotation and _getAnnotation

You cannot be sure that _getAnnotation will be called after _hasAnnotation check. You cannot be sure which of them will be used to check your replaced annotation (@JsonIgnore in my case) without looking into JacksonAnnotationIntrospector code. It seems that overriding them consistently would be a good practice. So we should also add the following override to our class if we want to use this approach:

override fun <A : Annotation> _hasAnnotation(
    annotated: Annotated,
    annoClass: Class<A>
): Boolean {
    if (annoClass == JsonIgnore::class.java && _hasAnnotation(annotated, MyJsonIgnore::class.java)) {
        return true
    }
    return super._hasAnnotation(annotated, annoClass)
}
  1. _getAnnotation return type should be nullable

This was correctly fixed by Kirill but was not explicitly pointed out. _getAnnotation can and will return null sometimes.

  1. You (probably) cannot have one magic MyConditional annotation.

Kirill's answer may encourage you to create something like a conditional annotation for all the jackson annotation which could be used as follows:

@MyConditional([
    JsonIgnore, JsonProperty("propertyName")
])

You just cannot have polymorphic annotation parameter. You would have to create My* for every Jackson annotation you need and for annotation with parameters it's not as neat as with @MyJsonIgnore.

You can try to make a repeatable annotation which would be applied like below and instantiated using reflection.

@MyConditional(
    clazz = JsonProperty::class.java,
    args = [
        // Ah, you probably cannot have an array of any possible args here, forget it.
    ]
)

  1. _hasAnnotation and _getAnnotation are not the only ways which JacksonAnnotationIntrospector uses to get or check for annotations

After using similar approach to create conditional @JsonProperty annotation I've noticed that it does not work for enum elements. After some debugging I found out that findEnumValues method uses java.lang.reflect.Field::getAnnotation directly (due to "various reasons" mentioned in deprecated findEnumValue). If you want your conditional annotation to work you should override (at least) findEnumValues.

  1. Be careful with ObjectMapper::setAnnotationIntrospector

Well, its Javadoc explicitly says it: be careful. It replaces the whole annotation introspector of your mapper removing all the added (chained) by modules Introspectors. It did not appear in my code in the question (it was for the sake of creating minimal example) but actually I've accidentally broke deserialization with KotlinModule. You may want to consider implementing JacksonModule and appending your introspector to existing ones.

  1. Consider another approach: implementing functionality-specific methods in NopAnnotationIntrospector.

At the end I ended up with this approach (mostly because of 4.). I needed to override findEnumValues and hasIgnoreMarker and it was enough for me. It involved a bit of copy-paste code from JacksonAnnotationMapper but unless you have to make a lot of the annotation conditional it might work (in any case implementing it involves a lot of boilerplate code). This way it's likely that you really want to chain this introspector, not to set it.

Related