Skip to content
Advertisement

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?

Advertisement

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.

7 People found this is helpful
Advertisement