1

I need to create a custom serializer that conditionally skips fields. In contrast to the case described in Skip objects conditionally when serializing with jackson my class contains a POJO member. A PersonalInfo has a Address as member. In case the Address is hidden the resulting JSON still has the "address" tag, but without value. I could not figure out how to fix this.

Creating a custom serializer on the ObjectMapper (see 3. at http://www.baeldung.com/jackson-custom-serialization) leads to the exact same result.

Here is the adapted code from the referenced question that shows the problem:

public class JacksonHide {
    @JsonIgnoreProperties("hidden")
    public static interface IHideable {
        boolean isHidden();
    }

    public static class Address implements IHideable {
        public final String city;
        public final String street;
        public final boolean hidden;

        public Address(String city, String street, boolean hidden) {
            this.city = city;
            this.street = street;
            this.hidden = hidden;
        }

        @Override
        public boolean isHidden() {
            return hidden;
        }
    }

    public static class PersonalInfo implements IHideable {
        public final String name;
        public final int age;
        public final Address address;
        public final boolean hidden;

        public PersonalInfo(String name, int age, Address address, boolean hidden) {
            this.name = name;
            this.age = age;
            this.address = address;
            this.hidden = hidden;
        }

        @Override
        public boolean isHidden() {
            return hidden;
        }
    }

    private static class MyBeanSerializerModifier extends BeanSerializerModifier {
        @Override
        public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
            if (IHideable.class.isAssignableFrom(beanDesc.getBeanClass())) {
                return new MyIHideableJsonSerializer((JsonSerializer<IHideable>) serializer);
            }
            return super.modifySerializer(config, beanDesc, serializer);
        }

        private static class MyIHideableJsonSerializer extends JsonSerializer<IHideable> {
            private final JsonSerializer<IHideable> serializer;

            public MyIHideableJsonSerializer(JsonSerializer<IHideable> serializer) {
                this.serializer = serializer;
            }

            @Override
            public void serialize(IHideable value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
                if (!value.isHidden()) {
                    serializer.serialize(value, jgen, provider);
                }

            }
        }
    }

    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setSerializerModifier(new MyBeanSerializerModifier());
        mapper.registerModule(module);

        PersonalInfo p1 = new PersonalInfo("John", 30, new Address("A", "B", false), false);
        PersonalInfo p2 = new PersonalInfo("Ivan", 20, new Address("C", "D", true), true);
        PersonalInfo p3 = new PersonalInfo("Mary", 40, new Address("C", "D", true), false);
        Address a1 = new Address("A", "B", false);
        Address a2 = new Address("C", "D", true);

        System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(Arrays.asList(p1, p2, p3, a1, a2)));
    }

}

UPDATE: Thanks to the feedback I have now a version based on @JSONFilter that gives me at least valid JSON. Unfortunately the nodes are still there, but are empty now ({}). How can I completely get rid of them?

public class JacksonFilterHide {

    @JsonFilter("HiddenFilter")
    @JsonIgnoreProperties("hidden")
    public static interface IHideable {
        boolean isHidden();
    }

    public static class Address implements IHideable {
        public final String city;
        public final String street;
        public final boolean hidden;

        public Address(String city, String street, boolean hidden) {
            this.city = city;
            this.street = street;
            this.hidden = hidden;
        }

        @Override
        public boolean isHidden() {
            return hidden;
        }
    }

    public static class PersonalInfo implements IHideable {
        public final String name;
        public final int age;
        public final Address address;
        public final boolean hidden;

        public PersonalInfo(String name, int age, Address address, boolean hidden) {
            this.name = name;
            this.age = age;
            this.address = address;
            this.hidden = hidden;
        }

        @Override
        public boolean isHidden() {
            return hidden;
        }
    }

    static final PropertyFilter hiddenFilter = new SimpleBeanPropertyFilter() {
        @Override
        public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer) throws Exception {
            if (include(writer)) {
                if (pojo instanceof IHideable && ((IHideable) pojo).isHidden()) {
                    return;
                } else {
                    writer.serializeAsField(pojo, jgen, provider);
                    return;
                }
            } else if (!jgen.canOmitFields()) { // since 2.3
                writer.serializeAsOmittedField(pojo, jgen, provider);
            }
        }

        @Override
        protected boolean include(BeanPropertyWriter writer) {
            return true;
        }

        @Override
        protected boolean include(PropertyWriter writer) {
            return true;
        }
    };

    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        // ObjectMapper mapper = UserInteractionModel.getMapper();
        FilterProvider filters = new SimpleFilterProvider().addFilter("HiddenFilter", hiddenFilter);
        mapper.setFilters(filters);
        mapper.enable(SerializationFeature.INDENT_OUTPUT);

        PersonalInfo p1 = new PersonalInfo("John", 30, new Address("A", "B", false), false);
        PersonalInfo p2 = new PersonalInfo("Ivan", 20, new Address("C", "D", true), true);
        PersonalInfo p3 = new PersonalInfo("Mary", 40, new Address("C", "D", true), false);
        Address a1 = new Address("A", "B", false);
        Address a2 = new Address("C", "D", true);

        System.out.println(mapper.writeValueAsString(Arrays.asList(p1, p2, p3, a1, a2)));
    }

}

Output now is:

[ { "name" : "John", "age" : 30, "address" : { "city" : "A", "street" : "B" } }, { }, { "name" : "Mary", "age" : 40, "address" : { } }, { "city" : "A", "street" : "B" }, { } ]

Expected:

[ { "name" : "John", "age" : 30, "address" : { "city" : "A", "street" : "B" } }, { "name" : "Mary", "age" : 40, }, { "city" : "A", "street" : "B" } ]

Update2 Temporary fix by traversing the tree and removing empty nodes. Ugly, but works for now. Still looking for a better answer.

private void removeEmptyNodes(JSONObject json) {
    Iterator<String> iter = json.keys();
    while (iter.hasNext()) {
        String key = iter.next();
        JSONObject node;
        try {
            node = json.getJSONObject(key);
        } catch (JSONException e) {
            continue;
        }
        if (node.length() == 0) {
            iter.remove();
        } else {
            removeEmptyNodes(node);
        }

    }
}

Solution inspired by this question: How do I remove empty json nodes in Java with Jackson?

Community
  • 1
  • 1
konfusius
  • 71
  • 1
  • 6
  • Getting rid of empty Objects is bit trickier, partly since although it is possible to enable exclusion of "empty" objects (this is detected via `JsonSerializer.isEmpty(...)`), this is not applied to arrays or `List`s. – StaxMan Jul 08 '15 at 03:56

1 Answers1

2

Your serializer is broken: it CAN NOT choose to NOT WRITE a value, if requested. In case of writing a property value, caller has already written out property name, so not writing the value will indeed break output. This either results in an exception being thrown (ideally), or broken output (less ideally); regardless, JsonSerializer is not allowed to try to decide whether value is being written.

To exclude a property being serialized, your valid choices include:

  1. Static property annotations like @JsonIgnore and @JsonIgnoreProperties that always exclude specific named property
  2. Static annotation @JsonInclude that bases inclusion on type of value (no nulls, no absents, no empty values)
  3. Static definition but dynamic choice of @JsonView to use (sets of properties you can dynamically exclude by associating with a view)
  4. Dynamic @JsonFilter
  5. Custom serializer for type that contains property
StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • Thanks you. The @JsonFilter did give me a small progress, but I'm still not done. See the update in the original question. – konfusius Jul 02 '15 at 14:42