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.