3

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

rnd
  • 321
  • 3
  • 13

2 Answers2

3

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;
      }

    }
rnd
  • 321
  • 3
  • 13
  • Please include explanation of what your code does and how it answers the question. If you get a code snippet as an answer, you may not know what to do with it. Answer should give the OP guidance on how to debug and fix their problem. Pointing out, what the idea behind your code is, greatly helps in understanding the issue and applying or modifying your solution. – Palec Feb 09 '14 at 17:51
3

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.