xmlMapper allow to use any root element during deserialization

1k Views Asked by At

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?

3

There are 3 best solutions below

0
On BEST ANSWER

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.

0
On

I'd approach this differently. Grab an XPath implementation, select all nodes that match //plainPassword, then get a list of contents of each node.

If you need to, you can also get the name of the parent node; when in context of a found node use .. to get the parent node.

Check XPath examples and try it out for yourself. Note that your code may differ depending on language and XPath implementation.

1
On

You can change your name of root class to everything, for example : @JacksonXmlRootElement(localName = "xyz") and it works.

Based on Java documentation JacksonXmlRootElement is used to define name of root element used for the root-level object when serialized (not for deserialized mapping), which normally uses name of the type (class).