33

I want to create a custom serializer which does a tiny bit of work and then leaves the rest for default serialization.

For example:

@JsonSerialize(using = MyClassSerializer.class)
public class MyClass {
  ...
}

public class MyClassSerializer extends JsonSerializer<MyClass> {
    @Override
    public void serialize(MyClass myClass, JsonGenerator generator, 
                          SerializerProvider provider) 
            throws JsonGenerationException, IOException {
        if (myClass.getSomeProperty() == someCalculationResult) {
            provider.setAttribute("special", true);
        }
        generator.writeObject(myClass);
    }  
}

With the idea of creating other custom serializers for aggregated objects which behave differently based on the 'special' attribute value. However, the above code does not work, as it unsurprisingly goes into an infinite recursion.

Is there a way to tell jackson to use default serialization once I have set the attribute? I don't really want enumerate all the properties like many custom serializers as the class is fairly complex and I don't want to have to do dual maintenance with the serializer every time I change the class.

DavidA
  • 3,984
  • 5
  • 25
  • 38
  • 2
    Not a duplicate, but probably worth reading: https://stackoverflow.com/questions/18313323/how-do-i-call-the-default-deserializer-from-a-custom-deserializer-in-jackson. The `BeanSerializerModifier` part seems to apply to serialization, too. – dhke Jun 25 '15 at 17:00
  • Worth reading: https://www.baeldung.com/jackson-call-default-serializer-from-custom-serializer , for why `BeanSerializerModifier` is necessary. – Anderson Jan 09 '22 at 07:10

4 Answers4

31

A BeanSerializerModifier will provide you access to the default serialization.

Inject a default serializer into the custom serializer

public class MyClassSerializer extends JsonSerializer<MyClass> {
    private final JsonSerializer<Object> defaultSerializer;

    public MyClassSerializer(JsonSerializer<Object> defaultSerializer) {
        this.defaultSerializer = checkNotNull(defaultSerializer);
    }

    @Override
    public void serialize(MyClass myclass, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (myclass.getSomeProperty() == true) {
            provider.setAttribute("special", true);
        }
        defaultSerializer.serialize(myclass, gen, provider);
    }
}

Create a BeanSerializerModifier for MyClass

public class MyClassSerializerModifier extends BeanSerializerModifier {
    @Override
    public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
        if (beanDesc.getBeanClass() == MySpecificClass.class) {
            return new MyClassSerializer((JsonSerializer<Object>) serializer);
        }
        return serializer;
    }
}

Register the serializer modifier

ObjectMapper om = new ObjectMapper()
        .registerModule(new SimpleModule()
                .setSerializerModifier(new MyClassSerializerModifier()));
Sam Berry
  • 7,394
  • 6
  • 40
  • 58
  • 3
    This removes the need for `@JsonSerialize` on the class. – Sam Berry Jun 25 '15 at 18:42
  • 1
    Is this solution still valid? I tried with both Jackson 2.5.0 and 2.8.1 and get: ...JsonMappingException: Class dto.ResponseSerializer has no default (no arg) constructor at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:282) at com.fasterxml.jackson.databind.SerializerProvider.mappingException(SerializerProvider.java:1123) at com.fasterxml.jackson.databind.SerializerProvider.reportMappingProblem(SerializerProvider.java:1145) at com.fasterxml.jackson.databind.SerializerProvider._createAndCacheUntypedSerializer(SerializerProvider.java:1227) – Billybong Aug 01 '16 at 13:16
  • @Billybong, remove the @JsonSerialize(using = MyClassSerializer.class) annotation from your class. – maxhuang Oct 14 '16 at 07:57
  • @SamB.hi, am using the first approach described in the answer. I get a compilation error saying checkNotNull is not resolvable. I added custom serializer for a specific type. But custom serializer cannot get access to default serializer instance. it is null! Could you help me how to get access. return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializerByType(TreeNode.class, new TreeNodeJsonSerializer()); – brownfox Oct 11 '17 at 06:34
  • 2
    This is not working for me. Although custom-serializer's serialize() method and setAttribute() within it are getting called, added (or modified) attribute is not reflected in final json output. (By the way, can setAttribute() add some extra field or only modify existing one?) – OwlR Dec 26 '18 at 13:10
  • @OwlR Hm, perhaps consider asking a new question so that you are able to provide more details of your specific problem? – Sam Berry Dec 27 '18 at 14:15
  • it's not working for me either - I mean adding new properties to result json. The only thing that worked is in this answer: https://stackoverflow.com/a/25360636/5268774 – devstructor Sep 11 '20 at 13:25
  • Very simple and useful answer. I know its too late, but IDEA now highlight `(JsonSerializer) serializer` as unchecked cast. Any workaround? – kami Apr 15 '22 at 05:03
  • @kami you can try alt/option + enter then select "Suppress for statement" or the appropriate selection. This sometimes works. Do alt + enter on the highlight and see the options it gives you. I get annoyed with the highlights too. – Sam Berry Apr 18 '22 at 20:45
3
@JsonSerialize(using = MyClassSerializer.class)
public class MyClass {
...
}

public class MyClassSerializer extends JsonSerializer<MyClass> {
    @Override
     public void serialize(MyClass myClass, JsonGenerator generator, 
                      SerializerProvider provider) 
        throws JsonGenerationException, IOException {
        if (myClass.getSomeProperty() == someCalculationResult) {
            provider.setAttribute("special", true);
        } else {
            provider.defaultSerializeValue(myClass, generator);
        }
    }  
}

if you are just writing an object as normal use the above

  • 3
    will this not return a stackOverflowError ? – Pranjal Gore Oct 24 '18 at 12:39
  • @PranjalGore no why would you think that? – LawrenceMouarkach Oct 24 '18 at 14:10
  • 6
    You are calling provider.defaultSerializer() for serialization of an object of MyClass which uses MyClassSerialzer for serialization. Now is it not possible that the default jackson serializer encounters @JsonSerialize annotation on the class and calls serialize method of MyClassSerialzer? This may form an unending loop. – Pranjal Gore Oct 24 '18 at 15:24
  • No I am calling provider.defaultSerializeValue() which is not the same as defaultSerializer(), this just checks if there's a condition then do something special , otherwise do the default in terms of serializing the value! It doesn't form an unending loop try it! – LawrenceMouarkach Oct 24 '18 at 16:21
  • 11
    This indeed throws a StackOverfliwError – SergioLeone Feb 04 '19 at 14:52
  • @SergioLeone what version of jackson are you using? are you using the provider.defaultSerializeValue() method not defaultSerializer()? thanks – LawrenceMouarkach Feb 05 '19 at 14:27
  • @LawrenceMouarkach I'm using 2.9.7 and yes, I'm calling `provider.defaultSerializeValue()` – SergioLeone Feb 05 '19 at 14:41
  • @SergioLeone could I see a code snippet please? I've never had an issue with this and would like to see the problem yourself and others are experiencing. – LawrenceMouarkach Feb 05 '19 at 14:43
0

You can use @JsonGetter instead of using a custom serializer if that's the only change you want to make.

public class MyClass{

    @JsonGetter("special")
    protected boolean getSpecialForJackson() {
        return myClass.getSomeProperty() == someCalculationResult;
    }

}
Alex Block
  • 21
  • 2
  • Thanks, @Alex, but I was setting an attribute flag to access during a later step of serialization. It wasn't the flag itself I was trying to serialize. – DavidA Jan 10 '19 at 19:10
0

To add to the chosen answer, the serializer implementation may also have to implement ContextualSerializer and ResolvableSerializer interfaces. Please take a look at a related issue here https://github.com/FasterXML/jackson-dataformat-xml/issues/259

public class MyClassSerializer extends JsonSerializer<MyClass>
    implements ContextualSerializer, ResolvableSerializer {
private final JsonSerializer<Object> defaultSerializer;

public MyClassSerializer(JsonSerializer<Object> defaultSerializer) {
    this.defaultSerializer = checkNotNull(defaultSerializer);
}

@Override
public void serialize(MyClass myclass, JsonGenerator gen, SerializerProvider provider)
        throws IOException {
    if (myclass.getSomeProperty() == true) {
        provider.setAttribute("special", true);
    }
    defaultSerializer.serialize(myclass, gen, provider);
}

@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
        throws JsonMappingException {
    if (defaultSerializer instanceof ContextualSerializer) {
        JsonSerializer<?> contextual = ((ContextualSerializer)defaultSerializer).createContextual(prov, property);
        return new MyClassSerializer((JsonSerializer<Object>)contextual);
    }
    return new MyClassSerializer(defaultSerializer);
}

@Override
public void resolve(SerializerProvider provider) throws JsonMappingException {
    if (defaultSerializer instanceof ResolvableSerializer) {
        ((ResolvableSerializer)defaultSerializer).resolve(provider);
    }
}

}

Also, if you are using Spring Boot, adding a Jackson module is as simple as

@Component
public class MyModule extends SimpleModule {

    @Override
    public void setupModule(SetupContext context) {
        context.addBeanSerializerModifier(new MyBeanSerializerModifier());
        super.setupModule(context);
    }
}
Pawel Zieminski
  • 439
  • 3
  • 8