Jackson: XML-Binding - Handling Null vs. Empty String

7.9k views Asked by At

The new Jackson-API provides us with convenient XML-Binding (just like JAXB for example), but i cant find any way to make Jackson serialize the typical "xsi:nil"-Attribute that is defacto standard to represent NULL-Values in XML? Please correct me if i see this wrong ;-)

In JAXB this can be done easily by annotating a java-variable with: @XMLElement(nillable=true)

see also: http://blog.bdoughan.com/2012/04/binding-to-json-xml-handling-null.html

Can Jackson do this ?

for Jackson-XML see: https://github.com/FasterXML/jackson-dataformat-xml

2

There are 2 answers

1
funkrusher On

This does not answer the question but provides a workaround (very hacky)!

I managed to write some custom serializers/deserializers for jackson (until jackson officially supports xsi:nil), that allow the following:

  • serialize Values from a POJO as xsi:nil elements to a XML-String if they are NULL in the POJO
  • deserialize a list of hardcoded Types (String,Integer,Float...) as NULL to a POJO if they are defined as xsi:nil elements in the given XML-String

With this code one can provide interoperability to other xml-binding libraries (JAXB..) that can only work with xsi:nil for null-values.

Class Item:

    public class Item {          
      public String x;
      public Integer y;        
      public Integer z;
    }

Class Main:

    public class Main {

      public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
        NumberDeserializers numberDeserializers = new NumberDeserializers();

        XmlMapper xmlMapper = new XmlMapper();    
        xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        // create custom-serialization
        XmlSerializerProvider provider = new XmlSerializerProvider(new XmlRootNameLookup());
        provider.setNullValueSerializer(new MyNullSerializer());
        xmlMapper.setSerializerProvider(provider);

        // create custom deserialization
        SimpleModule myModule = new SimpleModule("Module", new Version(1, 9, 10, "FINAL"));    
        myModule.addDeserializer(String.class, new NullableDeserializer(new StringDeserializer()));            
        myModule.addDeserializer(Number.class, new NullableDeserializer(numberDeserializers.find(Integer.class, Integer.class.getName())));
        myModule.addDeserializer(Float.class, new NullableDeserializer(numberDeserializers.find(Float.class, Float.class.getName())));

        xmlMapper.registerModule(myModule);  

        // deserialize
        Item value = xmlMapper.readValue(
            "<item xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ><a></a><x xsi:nil=\"true\"></x><y/><z>13</z></item>", 
            Item.class);

        // serialize
        String xml = xmlMapper.writeValueAsString(value); 
        System.out.println(xml);
      }
    }

Class MyNullSerializer:

    public class MyNullSerializer extends JsonSerializer<Object> {
      @Override
      public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
        ToXmlGenerator xGen = (ToXmlGenerator) jgen;

        xGen.writeStartObject();
        try {
          xGen.getStaxWriter().writeAttribute("xsi:nil", "true");
        } catch (Exception e){
          e.printStackTrace();
        }
        xGen.writeEndObject();    
      }
    }

Class MyNullDeserializer:

    public class MyNullDeserializer extends JsonDeserializer {

      private JsonDeserializer delegate; 

      public MyNullDeserializer(JsonDeserializer delegate){
        this.delegate = delegate;
      }

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

        FromXmlParser fxp = (FromXmlParser) jp;

        boolean isNil = false;

        XMLStreamReader reader = fxp.getStaxReader();
        if (reader.isStartElement()){
          if (reader.getAttributeCount() > 0){
            String atVal = reader.getAttributeValue("http://www.w3.org/2001/XMLSchema-instance", "nil");
            if (atVal != null){
              if (Boolean.parseBoolean(atVal) == true){
                isNil = true;
              }
            }
          }
        }

        Object value = null;
        if (isNil == false){
          value = delegate.deserialize(jp, ctxt);
        } else {
          jp.getValueAsString(); // move forward
        }
        return value;
      }

    }
0
Rodney Pantonial On

I expanded on the work of rnd since it enables the feature for all fields and not just some of them.

This is a module you will add to your bindings as follows:

XmlMapper mapper = new XmlMapper();
    
XmlSerializerProvider provider = new XmlSerializerProvider(new XmlRootNameLookup());
provider.setNullValueSerializer(new NullSerializer());
mapper.setSerializerProvider(provider);
mapper.registerModule(new NullPointerModule());

NullPointerModule implements its own customized serializer to pass a property needed for introspection of the current field.

NullPointerModule.java:

public class NullPointerModule extends SimpleModule implements java.io.Serializable {
    private static final long serialVersionUID = 1L;

    @Override
    public void setupModule(SetupContext context) {
        // Need to modify BeanDeserializer, BeanSerializer that are used
        context.addBeanSerializerModifier(new XmlBeanSerializerModifier() {
            @Override
            public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
                for (int i = 0, len = beanProperties.size(); i < len; ++i) {
                    BeanPropertyWriter bpw = beanProperties.get(i);
                    if (bpw.getClass().equals(BeanPropertyWriter.class)) {
                        beanProperties.set(i, new NullCheckedBeanPropertyWriter(bpw));
                    }
                }
                return beanProperties;
            }
        });
        super.setupModule(context);
    }

}

Next is the actual NullSerializer, this accepts the property writer and determines if the field does need the nil field or not.

NullSerializer.java:

public class NullSerializer extends JsonSerializer<Object> {

    @SuppressWarnings("unused")
    public void serializeWithProperty(BeanPropertyWriter propertyWriter, Object value, JsonGenerator jgen, SerializerProvider provider) {
        ToXmlGenerator xGen = (ToXmlGenerator) jgen;
        XmlElement annotation = null;

        if (propertyWriter != null) {
            AnnotatedMember member = propertyWriter.getMember();
            annotation = member.getAnnotation(XmlElement.class);
        }

        try {
            if (annotation != null) {
                if (annotation.nillable()) {
                    xGen.writeStartObject();
                    XMLStreamWriter staxWriter = xGen.getStaxWriter();

                    staxWriter.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
                    staxWriter.writeAttribute("xsi:nil", "true");
                    xGen.writeEndObject();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        serializeWithProperty(null, value, jgen, provider);
    }
}

Lastly is the override for the propertyWriters. This is a bit of a hack since this can fail if the property writer itself was replaced by another class in another module.

NullCheckedBeanPropertyWriter.java:

public class NullCheckedBeanPropertyWriter extends BeanPropertyWriter {
    public NullCheckedBeanPropertyWriter(BeanPropertyWriter base) {
        super(base);
    }

    @Override
    public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
        final Object value = (_accessorMethod == null) ? _field.get(bean)
                : _accessorMethod.invoke(bean);

        // Null handling is bit different, check that first
        if (value == null) {
            if (_nullSerializer != null) {
                gen.writeFieldName(_name);
                if (_nullSerializer instanceof NullSerializer) {
                    NullSerializer nullSerializer = (NullSerializer) _nullSerializer;
                    nullSerializer.serializeWithProperty(this, bean, gen, prov);
                    return;
                }
                _nullSerializer.serialize(null, gen, prov);
            }
            return;
        }
        super.serializeAsField(bean, gen, prov);
    }
}

The fields can then be added with @XmlElement(nillable=true) to make them work to your needs.