javaxmlxmlmapper

xmlMapper allow to use any root element during deserialization


I have such code

public class Xml {

    public static void main(String[] args) throws JsonProcessingException {

        String xmlString = "<password><plainPassword>12345</plainPassword></password>";

        XmlMapper xmlMapper = new XmlMapper();
        PlainPassword plainPassword = xmlMapper.readValue(xmlString, PlainPassword.class);
        System.out.println(plainPassword.getPlainPassword());
    }

    @JacksonXmlRootElement(localName = "password")
    public static class PlainPassword {

        public String getPlainPassword() {
            return this.plainPassword;
        }

        public void setPlainPassword(String plainPassword) {
            this.plainPassword = plainPassword;
        }

        private String plainPassword;
    }
}

It works fine, but in xmlString I can use any root tag name and my code still will work. For example String xmlString = "<x><plainPassword>12345</plainPassword></x>"; where I use x as root element also works. But is it possible to say xmlMapper that it could correctly deserialize only strings with "password" root element?


Solution

  • Unfortunately, the behavior you described is the one supported by Jackson as indicated in this Github open issue.

    With JSON content and ObjectMapper you can enable the UNWRAP_ROOT_VALUE deserialization feature, and maybe it could be of help for this purpose, although I am not quite sure if this feature is or not correctly supported by XmlMapper.

    One possible solution could be the implementation of a custom deserializer.

    Given your PlainPassword class:

    @JacksonXmlRootElement(localName = "password")
    public class PlainPassword {
    
      public String getPlainPassword() {
        return this.plainPassword;
      }
    
      public void setPlainPassword(String plainPassword) {
        this.plainPassword = plainPassword;
      }
    
    
      private String plainPassword;
    }
    

    Consider the following main method:

    public static void main(String[] args) throws JsonProcessingException {
    
      String xmlString = "<x><plainPassword>12345</plainPassword></x>";
    
      XmlMapper xmlMapper = new XmlMapper();
      xmlMapper.registerModule(new SimpleModule().setDeserializerModifier(new BeanDeserializerModifier() {
            @Override
            public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
              Class<?> beanClass = beanDesc.getBeanClass();
              JacksonXmlRootElement annotation = beanClass.getAnnotation(JacksonXmlRootElement.class);
              String requiredLocalName = null;
              if (annotation != null) {
                requiredLocalName = annotation.localName();
              }
    
              if (requiredLocalName != null) {
                return new EnforceXmlElementNameDeserializer<>(deserializer, beanDesc.getBeanClass(), requiredLocalName);
    
              }
              return deserializer;
            }
          }));
    
      PlainPassword plainPassword = xmlMapper.readValue(xmlString, PlainPassword.class);
      System.out.println(plainPassword.getPlainPassword());
    }
    

    Where the custom deserializer looks like:

    public class EnforceXmlElementNameDeserializer<T> extends StdDeserializer<T> implements ResolvableDeserializer {
    
      private final JsonDeserializer<?> defaultDeserializer;
      private final String requiredLocalName;
    
      public EnforceXmlElementNameDeserializer(JsonDeserializer<?> defaultDeserializer, Class<?> beanClass, String requiredLocalName) {
        super(beanClass);
        this.defaultDeserializer = defaultDeserializer;
        this.requiredLocalName = requiredLocalName;
      }
    
      @Override
      public T deserialize(JsonParser p, DeserializationContext ctxt)
          throws IOException {
        String rootName = ((FromXmlParser)p).getStaxReader().getLocalName();
        if (!this.requiredLocalName.equals(rootName)) {
          throw new IllegalArgumentException(
            String.format("Root name '%s' does not match required element name '%s'", rootName, this.requiredLocalName)
          );
        }
    
        @SuppressWarnings("unchecked")
        T itemObj = (T) defaultDeserializer.deserialize(p, ctxt);
        return itemObj;
      }
    
      @Override public void resolve(DeserializationContext ctxt) throws JsonMappingException {
        ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
      }
    }
    

    You have to implement ResolvableDeserializer when modifying BeanDeserializer, otherwise deserializing throws exception.

    The code is based in this excellent SO answer.

    The test should raise IllegalArgumentException with the corresponding message:

    Root name 'x' does not match required element name 'password'
    

    Please, modify the exception type as appropriate.

    If, instead, you use:

    String xmlString = "<password><plainPassword>12345</plainPassword></password>";
    

    in your main method, it should run without problem.