Search code examples
javajsonserializationfasterxml

How to use separate custom json serializers for parent and child classes?


In the below example, I have a primary class - A and its subclass - B:

public class A
{
   public final String primaryKey;

   A(String primaryKey)
   {
        this.primaryKey = primaryKey;
   }
}

public class B extends A
{
   public final String secondaryKey;

   B(String primaryKey, String secondaryKey)
   {
        super(primaryKey);
        this.secondaryKey = secondaryKey;
   }
}

Also I have custom serializer:

public class ASerializer extends StdSerializer<A> {
    public ASerializer(Class<A> t)
         super(t);
    }

    @Override
    public void serialize(A value, JsonGenerator jgen, SerializerProvider provider) {
        try {
            jgen.writeStartObject();
            jgen.writeFieldName("base");
            jgen.writeStartObject();
            jgen.writeStringField("CustomKeyA", value.primaryKey);
            jgen.writeEndObject();
            jgen.writeEndObject();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

So, the following code uses the custom serializer for json serialization:

ObjectMapper mapper = new ObjectMapper();
SimpleModule testModule = new SimpleModule("testModule", Version.unknownVersion());
testModule.addSerializer(A.class, new ASerializer(A.class));
result = mapper.writeValueAsString(new B("A", "B"));
System.out.println(result);

Actual output:

{
    "base" : {
         "CustomKeyA":"A"
    }
}

Expexted output:

{
    "base" : {
         "CustomKeyA":"A"
    },
    "secondaryKey":"B"
}

So, how can I serialize a super class members in custom way and all other child class members in standard way? Thanks.

UPD: Corrected.


Solution

  • If your use case is limited to only defining a specific name for a field of class A, then you do not need to implement and register a custom serializer. The desired output is achievable by adding

    @JsonProperty("CustomKeyA")
    public final String primaryKey;
    

    Both primaryKey and secondaryKey properties will be processed by Jackson for class B and included into the output as CustomKeyA and secondaryKey respectively.

    Using a custom serializer gives total control over the elements to output, bypassing the default serialization mechanism: it means that the properties will have to be handled "manually", which can easily become complicated for class hierarchies.

    Update

    If you are not allowed to modify the original classes A or B, there still can be a work-around using the serializer.

    The main difficulty of the approach is that the serializer for the parent class A does not know much about the properties of objects that it might receive that inherit from A. It would be very hard to check for particular type, cast and output the corresponding fields. It will be especially hellish to maintain.

    The idea of the solution that I can suggest is: retrieve the properties of the object as a Map using Jackson's mechanics (so that all the outputtable properties would be there) and replace the one of the parent class with appropriate key. Then output the map into JSON.

    This can be implemented as follows:

    public class ASerializer extends StdSerializer<A> {
    
      // we'll use this mapper to convert the object into a Map
      private static final ObjectMapper MAPPER = new ObjectMapper();
    
      // contains the list of property names that belong to parent class only
      private static final Set<String> BASE_PROPERTIES = new HashSet<>(Arrays.asList("primaryKey"));
    
      public ASerializer() {
        this(A.class);
      }
    
      public ASerializer(Class<A> t) {
        super(t);
      }
    
      private void serializeBaseProperties(A value, JsonGenerator jgen) throws IOException {
        // create a Map of base properties and their values to serialize under "base"
        final Map<String, Object> baseProperties = new HashMap<>();
        baseProperties.put("CustomKeyA", value.primaryKey);
        // output the map
        jgen.writeFieldName("base");
        jgen.writeObject(baseProperties);
      }
    
      @SuppressWarnings("unchecked")
      private void serializeOwnProperties(A value, JsonGenerator jgen) {
        ((Map<String, Object>) MAPPER.convertValue(value, Map.class)) // grab all serializable properties
          .entrySet().stream()
          .filter(entry -> !BASE_PROPERTIES.contains(entry.getKey())) // filter out the ones from base class
          .forEach(property -> writeProperty(property, jgen)); // output own properties to JSON
      }
    
      @Override
      public void serialize(A value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        jgen.writeStartObject();
        serializeBaseProperties(value, jgen);
        serializeOwnProperties(value, jgen);
        jgen.writeEndObject();
      }
    
      private void writeProperty(Map.Entry<String, Object> property, JsonGenerator jgen) {
        try {
          jgen.writeFieldName(property.getKey());
          jgen.writeObject(property.getValue());
        } catch (IOException ex) {
          throw new IllegalStateException(ex);
        }
      }
    
    }
    

    There might be another approach, by creating a hierarchy of serializers, in parallel with the target classes hierarchy. That will also work, but once again, that creates redundancies and makes it harder to maintain.

    End-of-update