Haster
Haster

Reputation: 165

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?

Upvotes: 4

Views: 1233

Answers (3)

jccampanero
jccampanero

Reputation: 53461

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.

Upvotes: 1

Dariusz
Dariusz

Reputation: 22291

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.

Upvotes: 1

Majid Hajibaba
Majid Hajibaba

Reputation: 3270

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).

Upvotes: 1

Related Questions