11

Given the following class hierarchy, I would like Foo to be serialized differently depending on the context it is used in my class hierarchy.

public class Foo {
    public String bar;
    public String biz;
}

public class FooContainer {
    public Foo fooA;
    public Foo fooB;
}

I would like for the biz attribute to not show up in fooB when I serialize FooContainer. So the output would look something like the following.

{
  "fooA": {"bar": "asdf", "biz": "fdsa"},
  "fooB": {"bar": "qwer"}
}

I was going to use something JsonView, but that has to be applied at the mapper layer for all instances of a class, and this is context dependent.


On the Jackson user mailing list, Tatu gave the simplest solution (works in 2.0), which I will probably end up using for now. Awarding the bounty to jlabedo because the answer is an awesome example of how to extend Jackson using custom annotations.

public class FooContainer {
    public Foo fooA;

    @JsonIgnoreProperties({ "biz" })
    public Foo fooB;
}
Ransom Briggs
  • 3,025
  • 3
  • 32
  • 46

4 Answers4

11

You could use a combination of a custom serializer with a custom property filter using JsonViews. Here is some code working with Jackson 2.0

Define a custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FilterUsingView {
    Class<?>[] value();
}

Define some Views:

// Define your views here
public static class Views {
    public class Public {};
    public class Internal extends Public{};
}

Then you can write your entities like this. Note that you may define your own annotation instead of using @JsonView :

public class Foo {
    @JsonView(Views.Public.class)
    public String bar;
    @JsonView(Views.Internal.class)
    public String biz;
}

public class FooContainer {
    public Foo fooA;
    @FilterUsingView(Views.Public.class)
    public Foo fooB;
}

Then, here is where the code begins :) First your custom filter:

public static class CustomFilter extends SimpleBeanPropertyFilter {

    private Class<?>[] _nextViews;

    public void setNextViews(Class<?>[] clazz){
        _nextViews = clazz;
    }

    @Override
    public void serializeAsField(Object bean, JsonGenerator jgen,
            SerializerProvider prov, BeanPropertyWriter writer)
            throws Exception {

        Class<?>[] propViews = writer.getViews();

        if(propViews != null && _nextViews != null){
            for(Class<?> propView : propViews){
                System.out.println(propView.getName());
                for(Class<?> currentView : _nextViews){
                    if(!propView.isAssignableFrom(currentView)){
                        // Do the filtering!
                        return;
                    }
                }
            }
        }
        // The property is not filtered
        writer.serializeAsField(bean, jgen, prov);
    }
}

Then a custom AnnotationIntrospector that will do two things:

  1. Enable your custom filter for any bean... Unless another filter is defined on your class (so you cannot use both of them, if you see what I mean)
  2. Enable your CustomSerializer if he found a @FilterUsingView annotation.

Here is the code

public class CustomAnnotationIntrospector extends AnnotationIntrospector {
    @Override
    public Version version() {
        return DatabindVersion.instance.version();
    }

    @Override
    public Object findFilterId(AnnotatedClass ac) {
      // CustomFilter is used for EVERY Bean, unless another filter is defined
      Object id = super.findFilterId(ac);
      if (id == null) {
        id = "CustomFilter";
      }
      return id;
    }

    @Override
    public Object findSerializer(Annotated am) {
        FilterUsingView annotation = am.getAnnotation(FilterUsingView.class);
        if(annotation == null){
            return null;
        }
        return new CustomSerializer(annotation.value());
    }
}

Here is your custom serializer. The only thing it does is passing your annotation's value to your custom filter, then it let the default serializer do the job.

public class CustomSerializer extends JsonSerializer<Object> {

    private Class<?>[] _activeViews;

    public CustomSerializer(Class<?>[] view){
        _activeViews = view;
    }

    @Override
    public void serialize(Object value, JsonGenerator jgen,
            SerializerProvider provider) throws IOException,
            JsonProcessingException {

        BeanPropertyFilter filter = provider.getConfig().getFilterProvider().findFilter("CustomFilter");
        if(filter instanceof CustomFilter){
            CustomFilter customFilter = (CustomFilter) filter;

            // Tell the filter that we will filter our next property
            customFilter.setNextViews(_activeViews);

            provider.defaultSerializeValue(value, jgen);

            // Property has been filtered and written, do not filter anymore
            customFilter.setNextViews(null);
        }else{
            // You did not define a CustomFilter ? Well this serializer is useless...
            provider.defaultSerializeValue(value, jgen);
        }
    }
}

Finally ! Let's put this all together :

public class CustomModule extends SimpleModule {

    public CustomModule() {
        super("custom-module", new Version(0, 1, 0, "", "", ""));
    }

    @Override
    public void setupModule(SetupContext context) {
        super.setupModule(context);
        AnnotationIntrospector ai = new CustomAnnotationIntrospector();
        context.appendAnnotationIntrospector(ai);
    }

}



@Test
public void customField() throws Exception {
    FooContainer object = new FooContainer();
    object.fooA = new Foo();
    object.fooA.bar = "asdf";
    object.fooA.biz = "fdsa";
    object.fooB = new Foo();
    object.fooB.bar = "qwer";
    object.fooB.biz = "test";

    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new CustomModule());

    FilterProvider fp = new SimpleFilterProvider().addFilter("CustomFilter", new CustomFilter());
    StringWriter writer = new StringWriter();

    mapper.writer(fp).writeValue(writer, object);

    String expected = "{\"fooA\":{\"bar\":\"asdf\",\"biz\":\"fdsa\"},\"fooB\":{\"bar\":\"qwer\"}}";

    Assert.assertEquals(expected, writer.toString());
}
jlabedo
  • 1,106
  • 8
  • 8
  • Awesome, I'm going to try and integrate your code this morning, and assuming it works I'll award the bounty. – Ransom Briggs Oct 08 '12 at 14:15
  • Thank you for the bounty, and thank you for reporting that @JsonIgnoreProperties can be applied to properties (new since 2.0)! I have some refactoring to do now... However, my solution is still useful if you have a lot of properties to filter. – jlabedo Oct 08 '12 at 17:14
  • One downside I see in your example is that the way you are using _nextViews appears to not be threadsafe. I'll end up doing something like your example, just with some tweaks. – Ransom Briggs Oct 08 '12 at 20:08
  • @RansomBriggs Hmm, I agree. I had already seen that but didn't have time and forgot. I ended up looking at BeanSerializerModifier.class. I think it can give a thread safe and much simpler implementation. I will look at it tomorrow morning. – jlabedo Oct 09 '12 at 00:49
  • You don't have to, I'm happy with your answer cause it got me to the right place, just noting it if someone else lands here. – Ransom Briggs Oct 09 '12 at 13:47
  • if it not a problem, please, do the refactoring with a multithreading. – Dmitry Mar 15 '13 at 07:52
0
import static org.junit.Assert.*;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Type;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.map.ser.SerializerBase;
import org.junit.Test;

class Foo {
    public String bar;
    public String biz;
}

class FooContainer {
    public Foo fooA;
    @JsonSerialize(using = FooCustomSerializer.class)
    public Foo fooB;
}

class FooCustomSerializer extends SerializerBase<Foo> {

    public FooCustomSerializer() {
        super(Foo.class);
    }

    @Override
    public void serialize(Foo foo, JsonGenerator generator, SerializerProvider provider) throws IOException, JsonGenerationException {
        generator.writeStartObject();
        generator.writeObjectField("bar", foo.bar);
        generator.writeEndObject();
    }

    @Override
    public JsonNode getSchema(SerializerProvider arg0, Type arg1) throws JsonMappingException {
        return null;
    }

}

public class JacksonTest {

    @Test
    public void customField() throws Exception {
        FooContainer object = new FooContainer();
        object.fooA = new Foo();
        object.fooA.bar = "asdf";
        object.fooA.biz = "fdsa";
        object.fooB = new Foo();
        object.fooB.bar = "qwer";
        object.fooB.biz = "test";
        ObjectMapper mapper = new ObjectMapper();
        StringWriter writer = new StringWriter();
        mapper.writeValue(writer, object);
        String expected = "{\"fooA\":{\"bar\":\"asdf\",\"biz\":\"fdsa\"},\"fooB\":{\"bar\":\"qwer\"}}";
        assertEquals(expected, writer.toString());
    }

}

Using @JsonSerialize(using = FooCustomSerializer.class) on the public Foo fooB; field.

http://jackson.codehaus.org/1.9.9/javadoc/org/codehaus/jackson/map/annotate/JsonSerialize.html

Fireblaze
  • 242
  • 3
  • 5
  • Trouble with this is that in the real world code there are many more attributes, I want all the rest to map naturally, and just to exclude this one field. That way I can add an attribute to Foo without having to remember to add the attribute to the serializer as well. – Ransom Briggs Oct 04 '12 at 18:35
  • That constraint was not in your question. Perhaps you can post your real question? – Fireblaze Oct 05 '12 at 07:32
  • Perhaps you are asking: http://stackoverflow.com/questions/12616730/how-to-use-jackson-serializer-for-domain-object-used-for-different-services – Fireblaze Oct 05 '12 at 11:06
0

I would use the google code gson
documentation in here https://code.google.com/p/google-gson/
Maven dependency is:

<dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.2.1</version>
        </dependency>

The annotations are like this:
To expose the field user the @Expose annotation
To generate a special name for the field in the parsed json user the @SerializedName("fieldNameInJSON") annotation
So your classes would look like this:

    public class Foo {
@SerializedName("bar")
    @Expose
        public String bar;
@SerializedName("biz")
    @Expose
        public String biz;
    }

    public class FooContainer {
@SerializedName("fooA")
    @Expose
        public Foo fooA;
@SerializedName("fooB")
    @Expose
        public Foo fooB;
    }

To serialize to JSON you will use a code that looks like this:

public String convertToJSON(FooContainer fc) {
        if (fc != null) {
            GsonBuilder gson = new GsonBuilder();
            return gson.excludeFieldsWithoutExposeAnnotation().create().toJson(fc);
        }
        return "";
    }

It would look the same for Lists for example:

public String convertToJSON(List<FooContainer> fcs) {
            if (fcs != null) {
                GsonBuilder gson = new GsonBuilder();
                return gson.excludeFieldsWithoutExposeAnnotation().create().toJson(fcs);
            }
            return "";
        }
Mr T.
  • 4,278
  • 9
  • 44
  • 61
  • 1
    Why is this answer down voted? Obviously the question is geared towards Jackson, but IMHO, Gson is a valid alternative, especially for people who have not fully committed their dependencies to Jackson. – Christopher Yang Oct 02 '14 at 18:36
0

The @JsonIgnoreProperties annotation can be used on the fooB property in FooContainer to ignore the biz property only in that specific context.

public class FooContainer {
    public Foo fooA;

    @JsonIgnoreProperties({ "biz" })
    public Foo fooB;
}

Note: You mentioned this in the edit you made back in 2012, but I'm writing this up as its own answer since I feel it's the best solution to this specific problem.

M. Justin
  • 14,487
  • 7
  • 91
  • 130