jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Change serializer applied to a single property of an class

Open Guy2You opened this issue 4 months ago • 4 comments

Is your feature request related to a problem? Please describe.

The current means (or at least the means that I used) of overriding the serializer on a single property of a class is convoluted.

Describe the solution you'd like

I would like a means of setting a custom serializer for a single property of a class where my custom serializer overrides any serializer applied to that property. Please delete this if it's a duplicate issue or if there is some way to do this that I missed.

Usage of a mixin for the class containing the property is inappropriate because I don't want to override the serialization of the class in any other way, just the property itself. Usage of SimpleModule.AddSerializer() for the property type is inappropriate because I don't want to override serialization of other properties of the same type.

Additionally I may only want to override the "regular" serializer and not the nullSerializer for the property.

I did find a way to accomplish this which I have provided an example of below, but it seems an easier way to accomplish this would be to remove the condition that blocks overriding the set serializer in a BeanPropertyWriter using the BeanPropertyWriter.assignSerializer(JsonSerializer<Object> ser) method, though I assume this is not that simple and that that condition is set for a reason.

Usage example

This is my workaround for the issue

// other json annotations here for this class
class MyClass {
  // this property should always be serialized using MyPropertySerializer
  @JsonSerialize(using = MyPropertySerializer.class)
  T myPropertyMySerialization = new MyProperty();

  // this property should be serialized using MyPropertySerializer when I don't want to override that behaviour below
  // BUT null values should be serialized with my NullSerializer
  @JsonSerialize(using = MyPropertySerializer.class, nullsUsing = NullSerializer.class)
  T myPropertyCustomSerialization = new MyProperty();
}

// elsewhere in the code
ObjectMapper myMethod() {
  SimpleModule module = new SimpleModule();
  module.setSerializerModifier(new BeanSerializerModifier() {

    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
      // modify serialization of MyClass
      if (MyClass.class.isAssignableFrom(beanDesc.getBeanClass())) {
        for (int i = 0; i < beanProperties.size(); i++) {
          BeanPropertyWriter beanPropertyWriter = beanProperties.get(i);
          // modify serialization of myPropertyCustomSerialization
          if ("myPropertyCustomSerialization".equals(beanPropertyWriter.getName()) {
            beanProperties.set(i, new MyCustomBeanPropertyWriter(beanPropertyWriter, new MyCustomPropertySerializer()));
          }
        }
        return beanProperties;
      }
      return super.changeProperties(config, beanDesc, beanProperties);
    }

    class MyCustomBeanPropertyWriter extends BeanPropertyWriter {
      private MyCustomBeanPropertyWriter(BeanPropertyWriter base, JsonSerializer<Object> newSerializer) {
        super(base);
        this._serializer = newSerializer;
      }
    }
  });
  JsonMapper.Builder builder = JsonMapper.builder();
  builder.addModule(module);
  return builder.build();
}

Additional context

Using version 2.15.3 of jackson-databind

Guy2You avatar Feb 14 '24 14:02 Guy2You

I am not sure why explicit override on property that you showed is insufficient:

class MyClass {
  // this property should always be serialized using MyPropertySerializer
  @JsonSerialize(using = MyPropertySerializer.class)
  T myPropertyMySerialization = new MyProperty();
}

since that is the specific mechanism for overriding things on per-property basis. It is also possible to use mix-ins to apply such annotation. And there shouldn't be any need to use BeanSerializerModifier: annotation alone should work.

So I feel I am missing something here.

cowtowncoder avatar Feb 14 '24 16:02 cowtowncoder

Rereading my question the following morning, you're right. My question was unclear and lacks important context, Sorry about that.

While Mixins would generally be an acceptable solution to this issue, in my case I'm not sure it is appropriate. I want to apply custom external configuration to the JsonMapper.Builder (above I'm doing this by adding my SimpleModule instance to the builder) to get the JsonMapper that it builds to serialize myPropertyCustomSerialization according to that configuration. I'm using this external configuration in the constructor of MyCustomPropertySerializer. I.e. on the line in the above code

beanProperties.set(i, new MyCustomBeanPropertyWriter(beanPropertyWriter, new MyCustomPropertySerializer()));

I am actually using the constructor new MyCustomPropertySerializer(configParameter). This serializer could look something like this.

class MyCustomPropertySerializer extends JsonSerializer<T> {
  private boolean serializeFirstProperty = true;
  
  // default no-arg constructor 
  public MyCustomPropertySerializer() {
    super();
  }
  
  // constructor to instantiate with my configuration
  public MyCustomPropertySerializer(boolean serializeFirstProperty) {
    this();
    this.serializeFirstProperty = serializeFirstProperty;
  }
  
  @Override
  public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) {
    if (serializeFirstProperty)
    {
      gen.writePOJOField("prop1", value);
    }
    // other serialization omitted
  }
}

So I may construct different JsonMappers to serialize MyClass.myPropertyCustomSerialization in different ways according to what I want to do.

I hope this adds enough context and my question makes more sense now.

Thanks.

Guy2You avatar Feb 15 '24 10:02 Guy2You

Ok. Yes, that makes more sense; thank you for the explanation.

As things are, most similar configurability (dynamic, non-annotation based) is attached to types (classes); specifically "config overrides" allow changing various aspects (like serialization inclusion) but are attached to types. There is currently no configurability mechanism that targets properties of POJOs, however. So that is something that would need to be added. It could work either similar to Config Overrides (i.e. register class + property-name + serializer (etc) on ObjectMapper), or, probably more likely, add an extension point for Modules to register a callback that asks for serializer (etc) associated with class + property-name combo; called by BeanSerializerFactory (or wherever this was resolved).

In the meantime, I do think use of BeanSerializerModifier makes sense for this tho.

cowtowncoder avatar Feb 16 '24 04:02 cowtowncoder

Thanks for your thoughts. I'll continue using the BeanSerializerModifier for this purpose in the way that I have done above. Personally I think that being able to register the serializer for the property on the Module makes the most sense to me. Creating an inner class to make use of BeanPropertyWriter's copy constructor and set the protected _serializer member feels like an awkward way to apply the wanted configuration.

Guy2You avatar Feb 16 '24 15:02 Guy2You