FasterXML / jackson-modules-base

Uber-project for foundational modules of Jackson that build directly on core components but nothing else; not including data format or datatype modules
Apache License 2.0
167 stars 77 forks source link

Registering JaxbAnnotationModule causes custom PropertyNamingStrategy to be ignored #137

Open skwirking opened 3 years ago

skwirking commented 3 years ago

I am trying to use the JaxbAnnotationModule when serialising a jaxb object to string. I am trying to achieve two things:

  1. enums should be serialised with the jaxb enum value, rather than the java enum name
  2. xml attributes names should be prefixed with the '@' symbol

I had hoped to use JaxbAnnotationModule to achieve 1. and a custom PropertyNamingStrategy to achieve 2. They work separately, but when I try to use both together, the custom PropertyNamingStrategy seems to never be invoked.

I don't know if my approach is wrong, or if this is a bug. Any help would be appreciated. Below is a pom.xml and junit 5 test that should reproduce the issue.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>jaxb-jackson</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.module</groupId>
      <artifactId>jackson-module-jaxb-annotations</artifactId>
      <version>2.12.3</version>
    </dependency>

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.7.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.7.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
package com.example.jaxbjackson;

import static org.junit.jupiter.api.Assertions.assertEquals;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlEnumValue;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;

public class JaxbToJsonConverterTest {

    @Test
    public void usesCustomPropertyNamingStrategy() throws Exception {
      AnXmlElement xmlelement = new AnXmlElement();
      xmlelement.setAnXmlAttribute(AnXmlAttributesEnumValue.VALUE_ONE);

      ObjectMapper mapper = new ObjectMapper();
      mapper.setPropertyNamingStrategy(new AtSymbolPropertyNamingStrategy());
      String json = mapper.writeValueAsString(xmlelement);

      String[] split = json.split(":");

      assertEquals("{\"@AnXmlAttribute\"", split[0]);
    }

    @Test
    public void usesJaxBAnnotationToDeriveEnumValue() throws Exception {
      AnXmlElement xmlelement = new AnXmlElement();
      xmlelement.setAnXmlAttribute(AnXmlAttributesEnumValue.VALUE_ONE);

      ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JaxbAnnotationModule());
      String json = mapper.writeValueAsString(xmlelement);

      String[] split = json.split(":");

      assertEquals("\"Value One\"}", split[1]);
    }

    @Test
    public void usesJaxBAnnotationAndCustomPropertyNamingStrategyTogether() throws Exception {
      AnXmlElement xmlelement = new AnXmlElement();
      xmlelement.setAnXmlAttribute(AnXmlAttributesEnumValue.VALUE_ONE);

      ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JaxbAnnotationModule());
      mapper.setPropertyNamingStrategy(new AtSymbolPropertyNamingStrategy());
      String json = mapper.writeValueAsString(xmlelement);

      String[] split = json.split(":");

      // below assertion fails because AtSymbolPropertyNamingStrategy is ignored
      assertEquals("{\"@AnXmlAttribute\"", split[0]);
      assertEquals("\"Value One\"}", split[1]);
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "")
    @XmlRootElement(name = "AnXmlElement")
    static class AnXmlElement {

        @XmlAttribute(name = "AnXmlAttribute")
        protected AnXmlAttributesEnumValue anXmlAttribute;

        public AnXmlAttributesEnumValue getAnXmlAttribute() {
            return anXmlAttribute;
        }

        public void setAnXmlAttribute(AnXmlAttributesEnumValue value) {
            this.anXmlAttribute = value;
        }
    }

    @XmlType(name = "anXmlAttributesEnumValues")
    @XmlEnum
    static enum AnXmlAttributesEnumValue {

        @XmlEnumValue("Value One")
        VALUE_ONE("Value One");

        private final String value;

        AnXmlAttributesEnumValue(String v) {
            value = v;
        }

        public String value() {
            return value;
        }

        public static AnXmlAttributesEnumValue fromValue(String v) {
            for (AnXmlAttributesEnumValue c: AnXmlAttributesEnumValue.values()) {
                if (c.value.equals(v)) {
                    return c;
                }
            }
            throw new IllegalArgumentException(v);
        }
    }

    @SuppressWarnings("serial")
    static class AtSymbolPropertyNamingStrategy extends PropertyNamingStrategy {

      private String fieldName(AnnotatedMember member, String defaultName) {
        XmlAttribute xmlAttributeAnnotation = member.getAllAnnotations().get(XmlAttribute.class);

        if (xmlAttributeAnnotation != null) {
          return "@" + xmlAttributeAnnotation.name();
        }

        return defaultName;
      }

      @Override
      public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
        return fieldName(method, defaultName);
      }

      @Override
      public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
        return fieldName(field, defaultName);
      }

      @Override
      public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
        return fieldName(method, defaultName);
      }
    }
}
cowtowncoder commented 3 years ago

Ah. I think I may know the issue here.

PropertyNamingStrategy is only applied to so-called "implicit" names: that is, names derived from getter/setter/field name. However, "explicit" names defined by things like annotations are not renamed by default: they are assumed to be exact names user wants, not subject to automatic renaming.

However, there is a setting:

MapperFeature.ALLOW_EXPLICIT_PROPERTY_RENAMING

enabling of which will change the behavior to apply naming strategy for such cases too.

So you may want to try that setting to see if it helps.

skwirking commented 3 years ago

Thank you very much for the explanation and the suggested fix. That does indeed cause the tests that I included in the description to pass.

However, in the real code I'm working with I find there is still a problem when using the custom PropertyNamingStrategy with properties that are annotated with @XmlID.

I created the following test to reproduce the error:

Invalid Object Id definition for com.example.jaxbjackson.JaxbToJsonConverterTest$AnXmlElement: cannot find property with name 'AnXmlAttribute'

Is there a conflict here between processing JaxB annotations and renaming explicit properties? If so, can you suggest how I might resolve it?

package com.example.jaxbjackson;

import static org.junit.jupiter.api.Assertions.assertEquals;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlEnumValue;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;

public class JaxbToJsonConverterTest {

    @Test
    public void usesJaxBAnnotationAndCustomPropertyNamingStrategyTogether() throws Exception {
      AnXmlElement xmlelement = new AnXmlElement();
      xmlelement.setAnXmlAttribute(AnXmlAttributesEnumValue.VALUE_ONE);

      ObjectMapper mapper = new ObjectMapper();
      mapper.registerModule(new JaxbAnnotationModule());
      mapper.setPropertyNamingStrategy(new AtSymbolPropertyNamingStrategy());
      mapper.configure(MapperFeature.ALLOW_EXPLICIT_PROPERTY_RENAMING, true);

      String json = mapper.writeValueAsString(xmlelement);

      String[] split = json.split(":");

      assertEquals("{\"@AnXmlAttribute\"", split[0]);
      assertEquals("\"Value One\"}", split[1]);
    }

    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "")
    @XmlRootElement(name = "AnXmlElement")
    static class AnXmlElement {

        @XmlAttribute(name = "AnXmlAttribute")
        protected AnXmlAttributesEnumValue anXmlAttribute;

        public AnXmlAttributesEnumValue getAnXmlAttribute() {
            return anXmlAttribute;
        }

        public void setAnXmlAttribute(AnXmlAttributesEnumValue value) {
            this.anXmlAttribute = value;
        }
    }

    @XmlType(name = "anXmlAttributesEnumValues")
    @XmlEnum
    static enum AnXmlAttributesEnumValue {

        @XmlEnumValue("Value One")
        @XmlID
        VALUE_ONE("Value One");

        private final String value;

        AnXmlAttributesEnumValue(String v) {
            value = v;
        }

        public String value() {
            return value;
        }

        public static AnXmlAttributesEnumValue fromValue(String v) {
            for (AnXmlAttributesEnumValue c: AnXmlAttributesEnumValue.values()) {
                if (c.value.equals(v)) {
                    return c;
                }
            }
            throw new IllegalArgumentException(v);
        }
    }

    @SuppressWarnings("serial")
    static class AtSymbolPropertyNamingStrategy extends PropertyNamingStrategy {

      private String fieldName(AnnotatedMember member, String defaultName) {
        XmlAttribute xmlAttributeAnnotation = member.getAllAnnotations().get(XmlAttribute.class);

        if (xmlAttributeAnnotation != null) {
          return "@" + xmlAttributeAnnotation.name();
        }

        return defaultName;
      }

      @Override
      public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
        return fieldName(method, defaultName);
      }

      @Override
      public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
        return fieldName(field, defaultName);
      }

      @Override
      public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
        return fieldName(method, defaultName);
      }
    }
}
skwirking commented 3 years ago

@cowtowncoder any suggestion on how to get past this issue with @XmlID?

cowtowncoder commented 3 years ago

@skwirking No. You may try mailing lists; I don't have time to dig into this at this point in time.