FasterXML/jackson-databind

Implement POJO serializer "decorator" abstraction to allow for augmenting POJO Serializations

Closed this issue · 22 comments

(note: also consider possibly allowing decorators for Collection/array types, scalars)


One recurring feature request is that of ability to add more "virtual" properties to serialize. Although it is relatively easy to modify behavior of existing properties (or anything that can be accessed via existing fields and/or methods), it is surprisingly difficult to add anything that "isn't there".

But it would be quite easy to allow addition of handlers called before and after serializing properties of POJOs (pre/post hooks); right after START_OBJECT and before END_OBJECT.

I think what is needed is:

  • New handler type that contains methods to call
  • Addition of a new property in @JsonSerialize to define handler to use

Another possible improvement (which could be deferred for future) would be to allow registering such handler externally. One challenge there is the question of how to match handler to type(s); as well as if and how to chain these.
Chaining would make sense to prevent modules from accidentally overriding/removing handlers; but might cause issues of its own.

I'd really love to see this. You mentioned that you might look into this for 2.3. Is this still on the road map?

@markwoon I haven't had any time to work further on this, but I do think it would be a great addition.

With 2.4.1 out now, this would at earliest get in 2.5.

can we assign the @JsonProperty(value="Dynamically From a Collection") while serilizing the json Object Thanks and regards Harikrishna Chigulla

@harikrishnac Ummm... what exactly would that mean?

Also, for general questions, please use mailing lists or user group forums. Git issues are for reporting problems, feature requests, and related discussion.

i mean can we have support for collections like @JsonProperty(value=ArryaList.get(1))
@cowtowncoder ok anyway thanks i will post in mailing list

Actually, #638 should cover the main use case: there is both annotation-based way of adding certain kinds of properties, and the underlying mechanism for further customizations.
While it does not work for arrays, Collections, I think I'll consider this as duplicate.
A new issue may be filed for further work, as necessary.

Adding a comment for people who end up here via http://jira.codehaus.org/browse/JACKSON-645 or http://jira.codehaus.org/browse/JACKSON-538 and are looking for a method which is called after a deserializer completes. I was able to achieve the desired effect by including an annotation and writing a converter which uses the same class as input and output.

@JsonDeserialize(converter=MyClassSanitizer.class)  // invoked after class is fully deserialized
public class MyClass {
    public String field1;
}

import com.fasterxml.jackson.databind.util.StdConverter;
public class MyClassSanitizer extends StdConverter<MyClass,MyClass> {
  @Override
  public MyClass convert(MyClass var1) {
    var.field1 = munge(var.field1);
    return var1;
  }
}

@sheavner Thank you for your work-around!

@sheavner This is perfect. @JsonDeserialize(converter=MyClassSanitizer.class) helps me tremendously!

Do you know if there is also a way to invoke a method BEFORE a class is deserialized?

@lemmingapex there is nothing specific for that, at this point. What kind of functionality would you want to invoke?

@cowtowncoder Thanks for your response.

I'm using the activejdbc ORM from javalite in combination with jersey and jackson. Here's a quick example that illustrates my need:

The construction of a Person requires that a connection to the database be open. Usually seen in code similar to this:

ActiveJDBC.open();
Person bob = new Person(); // person extends some activejdbc model
bob.setName("Bob");
ActiveJDBC.close();

So the construction of Bob needs to be wrapped in calls that open and close the database. Here's an example where jersey and jackson handle this workflow, and the construction (including deserialization) of a Person:

@PUT
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Person update(@NotNull @PathParam("id") String id, Person person) {
    // ... run the update ...
    return person;
}

So I would like a way to hook into the lifecycle of the construction of a Person. It seems like @JsonDeserialize(converter=...) will allow me to close the database connection. I'm looking for somewhere where I can add the open call to the database.

Does this make sense?

@lemmingapex yes, I think so. I ask because there's a vague, long-standing wish to have pre-/post-hooks for things, but more commonly (I think) it would be post-deserialization hook (and perhaps pre-serialization).

Perhaps it'd be possible to add something via @JsonGetter/@JsonSetter, to allow specifying such hooks.
If you want you could create a separate issue for adding such a callback; I don't think there is an open issue for that (if there is, I can merge separate issues, not a big deal to get duplicates).

@cowtowncoder Do you know of a workaround that I could use? Something like extending the default builder class, and adding the database open call in that lifecycle? Any ideas would be helpful.

@lemmingapex You could try wrapping+delegation route, via BeanDeserializerModifier: get original JsonDeserializer, hold on to ref -- then your deserializer would get deserialize() call, would dispatch to original one. There aren't many other hooks but that does work and could work well.

@cowtowncoder Thanks for your help. I set up a simple demo app to test this idea. I'm having a little trouble getting this deserialize() lifecycle working. Here's how I'm hooking into the lifecycle:

public class MyProvider implements ContextResolver<ObjectMapper> {

    final ObjectMapper defaultObjectMapper;

    public MyProvider() {
        defaultObjectMapper = createDefaultMapper();
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return defaultObjectMapper;
    }

    private static ObjectMapper createDefaultMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new BeanDeserializerModifier() {
            @Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, final JsonDeserializer<?> deserializer) {
                JsonDeserializer jsonDeserializer = new JsonDeserializer() {
                    @Override
                    public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
                        System.out.println("BEFORE!!!!" + deserializer.getClass());
                        return deserializer.deserialize(jsonParser, deserializationContext);
                    }
                };
                return jsonDeserializer;
            }
        });
        mapper.registerModule(module);

        return mapper;
    }

}

"BEFORE!!!!" is printed out, but the deserialize fails and returns this exception to the client:
No _valueDeserializer assigned
at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream@77652f59; line: 1, column: 2] through reference chain: com.example.example.services.Person["test"]

Here is the person class:

import com.fasterxml.jackson.annotation.JsonProperty;
import org.javalite.activejdbc.Model;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

@JsonDeserialize(converter = PersonPostMapper.class)
@XmlAccessorType(XmlAccessType.PROPERTY)
public class Person extends Model {

    private Integer test;

    public Person() {
        System.out.println("CTOR");
        this.test = 52;
    }

    @JsonProperty
    public Integer getTest() {
        return test;
    }

    @JsonProperty
    public void setTest(Integer test) {
        this.test = test;
    }

    @Override
    public String toString() {
        return "Person.toString() " + test;
    }
}

I'm unsure if I am passing the deserialize call off correctly. I can provide any of the source you would like to see. Anything you can think of would be helpful. Thank you very much!

@cowtowncoder Here is the servlet in web.xml as well:

<servlet>
    <servlet-name>Jersey REST Servlet</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
        <param-name>jersey.config.server.provider.packages</param-name>
        <param-value>com.example.example.services</param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.server.provider.classnames</param-name>
        <param-value>org.glassfish.jersey.jackson.JacksonFeature, com.example.example.MyProvider</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

Ok; there are 2 other things you do need to take care... you need to implement:

  1. ResolvableDeserializer
  2. ContextualDeserializer

which just need to delegate calls (one method each), in case delegate deserialize implements one or both. Also note that "resolve" modifies instance, whereas "createContextual()" either returns deserializer as is, or constructs a new one: in latter case you must take care to use the new instance.

I'm pretty sure exception you get is due to (2).

@cowtowncoder Is there anyway you can provide an example for what you mean with a ContextualDeserializer?

@lemmingapex how about go and grep for that in jackson-databind sources. There is something like StdDelegatingDeserializer that does this. You may even be able to extend that class.

For those looking for a way to add functionality before and after deserialization, this worked perfectly for my needs:

http://stackoverflow.com/questions/18313323/how-do-i-call-the-default-deserializer-from-a-custom-deserializer-in-jackson/18405958#18405958

Ah, glad you found that one. I should have bookmarked it. That's the way; but there may be small gotchas that are easy to overlook.

gavar commented

@lemmingapex

I'm unsure if I am passing the deserialize call off correctly. I can provide any of the source you would like to see. Anything you can think of would be helpful. Thank you very much!

You can use DelegatingDeserializer for that purpose.

public class LifecycleDeserializerModifier extends BeanDeserializerModifier {
    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        return new LifecycleDeserializer(deserializer);
    }
}

class LifecycleDeserializer extends DelegatingDeserializer {

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

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> delegate) {
        return new LifecycleDeserializer(delegate);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        final Object object = super.deserialize(p, ctxt);
        if (object instanceof JsonDeserializeAware)
            ((JsonDeserializeAware) object).afterDeserialize();
        return object;
    }
}