150

I have a problem in my custom deserializer in Jackson. I want to access the default serializer to populate the object I am deserializing into. After the population I will do some custom things but first I want to deserialize the object with the default Jackson behavior.

This is the code that I have at the moment.

public class UserEventDeserializer extends StdDeserializer<User> {

  private static final long serialVersionUID = 7923585097068641765L;

  public UserEventDeserializer() {
    super(User.class);
  }

  @Override
  @Transactional
  public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {

    ObjectCodec oc = jp.getCodec();
    JsonNode node = oc.readTree(jp);
    User deserializedUser = null;
    deserializedUser = super.deserialize(jp, ctxt, new User()); 
    // The previous line generates an exception java.lang.UnsupportedOperationException
    // Because there is no implementation of the deserializer.
    // I want a way to access the default spring deserializer for my User class.
    // How can I do that?

    //Special logic

    return deserializedUser;
  }

}

What I need is a way to initialize the default deserializer so that I can pre-populate my POJO before I start my special logic.

When calling deserialize from within the custom deserializer It seems the method is called from the current context no matter how I construct the serializer class. Because of the annotation in my POJO. This causes a Stack Overflow exception for obvious reasons.

I have tried initializing a BeanDeserializer but the process is extremely complex and I haven't managed to find the right way to do it. I have also tried overloading the AnnotationIntrospector to no avail, thinking that it might help me ignore the annotation in the DeserializerContext. Finally it seams I might have had some success using JsonDeserializerBuilders although this required me to do some magic stuff to get hold of the application context from Spring. I would appreciate any thing that could lead me to a cleaner solution for example how Can I construct a deserialization context without reading the JsonDeserializer annotation.

oberlies
  • 11,503
  • 4
  • 63
  • 110
Pablo Jomer
  • 9,870
  • 11
  • 54
  • 102
  • 2
    No. Those approaches will not help: the problem is that you will need a fully constructed default deserializer; and this requires that one gets built, and then your deserializer gets access to it. `DeserializationContext` is not something you should either create or change; it will be provided by `ObjectMapper`. `AnnotationIntrospector`, likewise, won't be of help in getting access. – StaxMan Aug 21 '13 at 21:16
  • How did you end up doing it in the end? – khituras Oct 29 '14 at 07:32
  • Good question. I'm not sure but I am certain the answer below helped me. I am currently not in the possession of the code that we wrote if you do find a solution please post it here for others. – Pablo Jomer Oct 29 '14 at 09:44
  • 3
    It is amazing to me how this was asked in 2013, and in the year of our lord 2022 there still is no sane solution for such an incredibly common requirement. – kaqqao May 19 '22 at 21:47
  • @kaqqao I agree. I didn't expect the framework to even be in use any more. – Pablo Jomer May 25 '22 at 05:39

11 Answers11

115

As StaxMan already suggested you can do this by writing a BeanDeserializerModifier and registering it via SimpleModule. The following example should work:

public class UserEventDeserializer extends StdDeserializer<User> implements ResolvableDeserializer
{
  private static final long serialVersionUID = 7923585097068641765L;

  private final JsonDeserializer<?> defaultDeserializer;

  public UserEventDeserializer(JsonDeserializer<?> defaultDeserializer)
  {
    super(User.class);
    this.defaultDeserializer = defaultDeserializer;
  }

  @Override public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException
  {
    User deserializedUser = (User) defaultDeserializer.deserialize(jp, ctxt);

    // Special logic

    return deserializedUser;
  }

  // for some reason you have to implement ResolvableDeserializer when modifying BeanDeserializer
  // otherwise deserializing throws JsonMappingException??
  @Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
  {
    ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
  }


  public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
  {
    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier()
    {
      @Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
      {
        if (beanDesc.getBeanClass() == User.class)
          return new UserEventDeserializer(deserializer);
        return deserializer;
      }
    });


    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(module);
    User user = mapper.readValue(new File("test.json"), User.class);
  }
}
schummar
  • 1,591
  • 1
  • 9
  • 9
  • Thanks! I already solved this in another way but I will look into your solution when i have more time. – Pablo Jomer Aug 24 '13 at 08:39
  • 7
    Is there a way to do the same but with a `JsonSerializer` ? I have several serializers but they have code on common so i want to generify it. I try to directly call The serializer but The result isn't unwrapped in the JSON result (each call of serializer create a new object) – herau Oct 17 '14 at 16:19
  • 1
    @herau `BeanSerializerModifier`, `ResolvableSerializer` and `ContextualSerializer` are the matching interfaces to use for serialization. – StaxMan Oct 04 '16 at 18:32
  • Is this applicable for EE edition containers (Wildfly 10)? I get JsonMappingException: (was java.lang.NullPointerException) (through reference chain: java.util.ArrayList[0]) – user1927033 Jun 15 '17 at 01:38
  • The question uses `readTree()` but the answer does not. What is the advantage of this approach versus the one [posted by Derek Cochran](https://stackoverflow.com/a/50123297/14731)? Is there a way to make this work with `readTree()`? – Gili Nov 01 '18 at 21:51
  • @PabloJomer Do you by any chance still remember what approach you ended up using? Perhaps one of the other answers offered here? – kaqqao Mar 01 '19 at 00:25
  • I'm sorry as I have previously stated I don't have access to the code any more. And I haven't been working with Jackson since. – Pablo Jomer Mar 01 '19 at 08:20
  • It is really nice, but is it possible to do something similar with jackson 1.x? I have to work on a legacy project with jackson 1.9.2, and that `SimpleModule` didn't have default constructor, neither `setDeserializerModifier()` method in the old times. Or any idea where can I find legacy jackson documentation? – Balázs Nemes Mar 19 '19 at 15:30
  • 1
    Implementing `resolve` is not necessary if you use a `DelegatingDeserializer` as base class - see [this answer](https://stackoverflow.com/a/55938316/1523648) – oberlies May 01 '19 at 15:04
  • Two important notes: 1- As oberlies said in his answer, registering the module is mutual exclusive with the @JsonDeserializer annotation, if you keep the annotation you will get "Missing 0 arg constructor" (something like this) error. 2 - The conditional in the modifySerializer method is actually VERY necessary (even if you are using the mapper just for your class), otherwise you start getting JVM level errors about modules and loaders that have nothing to do with anything. – Tocchetto Dec 14 '22 at 18:15
27

The DeserializationContext has a readValue() method you may use. This should work for both the default deserializer and any custom deserializers you have.

Just be sure to call traverse() on the JsonNode level you want to read to retrieve the JsonParser to pass to readValue().

public class FooDeserializer extends StdDeserializer<FooBean> {

    private static final long serialVersionUID = 1L;

    public FooDeserializer() {
        this(null);
    }

    public FooDeserializer(Class<FooBean> t) {
        super(t);
    }

    @Override
    public FooBean deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        FooBean foo = new FooBean();
        foo.setBar(ctxt.readValue(node.get("bar").traverse(), BarBean.class));
        return foo;
    }

}
Derek Cochran
  • 388
  • 3
  • 6
  • DeserialisationContext.readValue() does not exist, that is a method of ObjectMapper – Pedro Borges Jun 13 '18 at 15:24
  • 5
    this solution is working well, however you might need to call nextToken() if you deserialize a value class e.g. Date.class – revau.lt Sep 06 '18 at 08:57
  • 2
    Your solution is the most elegant one.You are delegating serialization dispatching of BarBean.class to Jackson. This is nice, you can make your deserializers small, re-usable, and testable. I believe instead of JsonNode.traverse() you should be calling JsonNode.traverse(codec) to pass on existing deserializers codec. – Vladimir Dec 22 '20 at 19:34
20

I found an answer at https://stackoverflow.com/a/51927577/14731 which is much more readable than the accepted answer.

public User deserialize(JsonParser jp, DeserializationContext ctxt)
    throws IOException, JsonProcessingException {
        User user = jp.readValueAs(User.class);
         // some code
         return user;
      }

It really doesn't get easier than this.

Gili
  • 86,244
  • 97
  • 390
  • 689
  • Hi Gili! Thanks for it I'm hoping that people find this answer and have time to validate it. I am no longer in a position to do so there for I can not accept the answer at this time. If I see that people do say this is a possible solution I will of course guide them towards it. It may also be that this is not possible for all versions. Still thanks for sharing. – Pablo Jomer Nov 04 '18 at 21:02
  • Does not compile with Jackson 2.9.9. JsonParser.readTree() does not exist. – ccleve Jan 08 '20 at 22:42
  • @ccleve Looks like a simple typo. Fixed. – Gili Jan 10 '20 at 00:09
  • 1
    Can confirm that this works with Jackson 2.10, thanks! – Stuart Leyland-Cole Mar 04 '20 at 12:43
  • 40
    I don't get how this works, this results in a `StackOverflowError`, since Jackson will use the same serializer again for `User`... – john16384 Jul 17 '20 at 13:04
  • @john16384, it depends on the use case. If you want to use another registered deserailizer, and modify the result, then it should work. If you use the same deserializer, you will have to do a work around like in the first answer, to get the original deserializer. – gagarwa Sep 09 '21 at 01:02
10

Along the lines of what Tomáš Záluský has suggested, in cases where using BeanDeserializerModifier is undesirable you can construct a default deserializer yourself using BeanDeserializerFactory, although there is some extra setup necessary. In context, this solution would look like so:

public User deserialize(JsonParser jp, DeserializationContext ctxt)
  throws IOException, JsonProcessingException {

    ObjectCodec oc = jp.getCodec();
    JsonNode node = oc.readTree(jp);
    User deserializedUser = null;

    DeserializationConfig config = ctxt.getConfig();
    JavaType type = TypeFactory.defaultInstance().constructType(User.class);
    JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));

    if (defaultDeserializer instanceof ResolvableDeserializer) {
        ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
    }

    JsonParser treeParser = oc.treeAsTokens(node);
    config.initialize(treeParser);

    if (treeParser.getCurrentToken() == null) {
        treeParser.nextToken();
    }

    deserializedUser = (User) defaultDeserializer.deserialize(treeParser, context);

    return deserializedUser;
}
magiccrafter
  • 5,175
  • 1
  • 56
  • 50
hopper
  • 13,060
  • 7
  • 49
  • 53
  • 1
    This works like a dream with Jackson 2.9.9. It doesn't suffer from a StackOverflowError like the other example given. – meta1203 Aug 19 '20 at 22:47
  • This is the only solutions that lets me to export my DTOs with some extra versioning logic. Module approach requires to register in all ObjectMappers (Hibernate uses custom ObjectMapper for example). Converters don't work with generic types. Hack with additional class is a little bit ugly. So current seems the best. –  Feb 09 '23 at 09:25
  • Thanks so much for your elegant solution! – Most Noble Rabbit Jun 12 '23 at 13:04
10

If it is possible for you to declare extra User class then you can implement it just using annotations

// your class
@JsonDeserialize(using = UserEventDeserializer.class)
public class User {
...
}

// extra user class
// reset deserializer attribute to default
@JsonDeserialize
public class UserPOJO extends User {
}

public class UserEventDeserializer extends StdDeserializer<User> {

  ...
  @Override
  public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    // specify UserPOJO.class to invoke default deserializer
    User deserializedUser = jp.ReadValueAs(UserPOJO.class);
    return deserializedUser;

    // or if you need to walk the JSON tree

    ObjectMapper mapper = (ObjectMapper) jp.getCodec();
    JsonNode node = oc.readTree(jp);
    // specify UserPOJO.class to invoke default deserializer
    User deserializedUser = mapper.treeToValue(node, UserPOJO.class);

    return deserializedUser;
  }

}
Bill
  • 409
  • 1
  • 6
  • 6
  • 2
    Yup. The only approach that worked for me. I was getting StackOverflowErrors because of a recursive call to the deserializer. – ccleve Jan 08 '20 at 22:58
  • Although this is some kind of hack it allows to use the default serializer for the know fields, while you still have access to the unknown ones. Thus, this can be used to read a csv with columns that should be deserialized to a Map (or a nested object). For example: ObjectMapper mapper = (ObjectMapper) jp.getCodec(); JsonNode node = oc.readTree(jp); User deserializedUser = mapper.treeToValue(node, UserPOJO.class); String userName = node.get("user.name").asText(); deserializedUser.setUserName(userName); return deserializedUser; – ampofila Dec 08 '20 at 15:46
  • @Bill you don't need the cast to ObjectMapper, treeToValue is inherited – João Matos Mar 01 '22 at 23:09
9

There are couple of ways to do this, but to do it right involves bit more work. Basically you can not use sub-classing, since information default deserializers need is built from class definitions.

So what you can most likely use is to construct a BeanDeserializerModifier, register that via Module interface (use SimpleModule). You need to define/override modifyDeserializer, and for the specific case where you want to add your own logic (where type matches), construct your own deserializer, pass the default deserializer you are given. And then in deserialize() method you can just delegate call, take the result Object.

Alternatively, if you must actually create and populate the object, you can do so and call overloaded version of deserialize() that takes third argument; object to deserialize into.

Another way that might work (but not 100% sure) would be to specify Converter object (@JsonDeserialize(converter=MyConverter.class)). This is a new Jackson 2.2 feature. In your case, Converter would not actually convert type, but simplify modify the object: but I don't know if that would let you do exactly what you want, since the default deserializer would be called first, and only then your Converter.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • My answer still stands: you need to let Jackson construct the default deserializer to delegate to; and have to find a way to "override" it. `BeanDeserializerModifier` is the callback handler that allows that. – StaxMan Aug 21 '13 at 21:17
5

You are bound to fail if you try to create your custom deserializer from scratch.

Instead, you need to get hold of the (fully configured) default deserializer instance through a custom BeanDeserializerModifier, and then pass this instance to your custom deserializer class:

public ObjectMapper getMapperWithCustomDeserializer() {
    ObjectMapper objectMapper = new ObjectMapper();

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                    BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer) 
            if (beanDesc.getBeanClass() == User.class) {
                return new UserEventDeserializer(defaultDeserializer);
            } else {
                return defaultDeserializer;
            }
        }
    });
    objectMapper.registerModule(module);

    return objectMapper;
}

Note: This module registration replaces the @JsonDeserialize annotation, i.e. the User class or User fields should no longer be annotated with this annotation.

The custom deserializer should then be based on a DelegatingDeserializer so that all methods delegate, unless you provide an explicit implementation:

public class UserEventDeserializer extends DelegatingDeserializer {

    public UserEventDeserializer(JsonDeserializer<?> delegate) {
        super(delegate);
    }

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
        return new UserEventDeserializer(newDelegate);
    }

    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        User result = (User) super.deserialize(p, ctxt);

        // add special logic here

        return result;
    }
}
oberlies
  • 11,503
  • 4
  • 63
  • 110
  • It failed for me with missing non-arg constructor: Caused by: java.lang.IllegalArgumentException: Class RecordDeserializer has no default (no arg) constructor. And super(delegatee) constructor requires non null argument. – Leos Literak Nov 27 '20 at 07:56
  • The "has no default (no arg) constructor" is because you've left over the @JsonDeserializer annotation. Registering the module is mutually exclusive with the annotation – Tocchetto Dec 14 '22 at 18:17
1

I was not ok with using BeanSerializerModifier since it forces to declare some behavioral changes in central ObjectMapper rather than in custom deserializer itself and in fact it is parallel solution to annotating entity class with JsonSerialize. If you feel it the similar way, you might appreciate my answer here: https://stackoverflow.com/a/43213463/653539

Community
  • 1
  • 1
Tomáš Záluský
  • 10,735
  • 2
  • 36
  • 64
1

Using BeanDeserializerModifier works well, but if you need to use JsonDeserialize there is a way to do it with AnnotationIntrospector like this:

ObjectMapper originalMapper = new ObjectMapper();
ObjectMapper copy = originalMapper.copy();//to keep original configuration
copy.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {

            @Override
            public Object findDeserializer(Annotated a) {
                Object deserializer = super.findDeserializer(a);
                if (deserializer == null) {
                    return null;
                }
                if (deserializer.equals(MyDeserializer.class)) {
                    return null;
                }
                return deserializer;
            }
});

Now copied mapper will now ignore your custom deserializer (MyDeserializer.class) and use default implementation. You can use it inside deserialize method of your custom deserializer to avoid recursion by making copied mapper static or wire it if using Spring.

0

A simpler solution for me was to just add another bean of ObjectMapper and use that to deserialize the object (thanks to https://stackoverflow.com/users/1032167/varren comment) - in my case I was interested to either deserialize to its id (an int) or the whole object https://stackoverflow.com/a/46618193/986160

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;

import java.io.IOException;

public class IdWrapperDeserializer<T> extends StdDeserializer<T> {

    private Class<T> clazz;

    public IdWrapperDeserializer(Class<T> clazz) {
        super(clazz);
        this.clazz = clazz;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper;
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        String json = jp.readValueAsTree().toString();
          // do your custom deserialization here using json
          // and decide when to use default deserialization using local objectMapper:
          T obj = objectMapper().readValue(json, clazz);

          return obj;
     }
}

for each entity that needs to be going through custom deserializer we need to configure it in the global ObjectMapper bean of the Spring Boot App in my case (e.g for Category):

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
            mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
            mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
            mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    SimpleModule testModule = new SimpleModule("MyModule")
            .addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))

    mapper.registerModule(testModule);

    return mapper;
}
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
0

Here is a short solution using default ObjectMapper

private static final ObjectMapper MAPPER = new ObjectMapper(); // use default mapper / mapper without customization

public MyObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    MyObject object = MAPPER.readValue(p, MyObject.class);
    // do whatever you want 
    return object;
}

And please: There is really no need to use any String value or something else. All needed information are given by JsonParser, so use it.

kaiser
  • 940
  • 1
  • 10
  • 25
  • 2
    This is definitely the simplest solution I've found, but having to create a whole new `ObjectMapper` just to get the default behavior back seems wrong. – Dan Lenski Nov 09 '20 at 22:24
  • 2
    You can make the object mapper a static final instance. – kaiser Nov 11 '20 at 05:54
  • It seems like what I need instead of ObjectMapper is "my object mapper without this custom deserializer installed", so that I still pick up other customizations. – Wheezil Oct 15 '21 at 17:33
  • This is definitely not the right answer, as creating a new `ObjectMapper` is always expensive – Francesco Guardiani Jan 07 '22 at 16:34
  • @FrancescoGuardiani Check my comment above, you can make the ObjectMapper a final static... I will edit the answer. – kaiser Jan 17 '22 at 08:57
  • Still, this solution is creating two separate object mappers, so it's error prone as you might need inside this additional mapper to register other custom ser/de for the objects nested within `MyObject`. I think the best solution that works for all is https://stackoverflow.com/a/55938316/2399055 – Francesco Guardiani Jan 21 '22 at 11:33