jackson-future-ideas icon indicating copy to clipboard operation
jackson-future-ideas copied to clipboard

Different way to specify values for `Include.NON_DEFAULT` and `JsonInclude.Include.CUSTOM`

Open fxshlein opened this issue 6 months ago • 0 comments

I myself (and from the issues I read, a lot of other people too) have had trouble with Include.NON_DEFAULT due to the fact that it's not really possible to properly extract default values. There is JsonInclude.Include.CUSTOM, but that is pretty clunky to use in some cases.

It's been mentioned before that the possibility of changing or removing Include.NON_DEFAULT is being considered for jackson 3.x. I'd just like to propose one way to potentially solve this issue, which would be to change the ways Include.NON_DEFAULT and Include.CUSTOM obtain the default value/filter.

For this, I would propose two new annotations:

  • @JsonDefaultValue("PropertyName")
  • @JsonIncludeFilter("PropertyName")

For Include.NON_DEFAULT, jackson would try to find a static field annotated with a matching @JsonDefaultValue annotation, and then use that as the default value. This could be very nice to use, as you would only need to move your default values into separate static fields, and specifying a default value as a static property is a pretty reasonable and established pattern already.

For Include.CUSTOM, jackson would try to find a member function annotated with a matching @JsonIncludeFilter annotation, which returns a boolean, and then only include the value if that function returns true. Being a member function, this could be even more powerful than the existing behavior for CUSTOM, because it could have access to the entire object being serialized.

A usage example:

data class MyClass(
    @JsonProperty("Prop1")
    @JsonInclude(Include.NON_DEFAULT) // <- Jackson will look for a static @JsonDefaultValue field
    val property1: String = default1,
    @JsonProperty("Prop2")
    @JsonInclude(Include.CUSTOM) // <- Jackson will look for a @JsonIncludeFilter method
    val property2: String = default2,
    @JsonProperty("Prop3")
    val property3: String, // <- This property has no default - the class cannot be constructed to extract defaults
) {
    @JsonIncludeFilter("Prop2") // <- The value will only be included if this returns 'true':
    private fun shouldIncludeProperty2(): Boolean {
               // Note how this has access to the whole object - I could also do something like 'property2 != property1'.
        return property2.equals(default2, ignoreCase = true)
    }

    companion object {
        @JvmStatic
        @JsonDefaultValue("Prop1") // <- The value will only be included if it's not equal to the value of this property:
        private val default1: String = "Hello"

        @JvmStatic
        private val default2: String = "World"
    }
}

One potential issue with this is that it would probably be hard to use these in meta-annotations, since they explicitly include a property name. Perhaps it could be possible to omit the property name, in which case the default applies for the whole type:

data class Test(
    val value: String
) {
    companion object {
        @JvmStatic
        @JsonDefaultValue // <- no property name, this is the default value for anything with the type "Test"
        val EMPTY = Test("")
    }
}

I think the "static field/method that returns a default value" pattern is also already quite common, so this would integrate fairly well into most code bases.

fxshlein avatar Dec 30 '23 17:12 fxshlein