javajacksondeserializationxmlmapper

Jackson ConstructorProperties ignores properties names


I am really confused how jackson (2.9.6 version) ObjectMapper works with @ConstructorProperties annotation.

It seems that mapper ignores property names which are present in a @ConstructorPropertiesannotation value method.

What's even more interesting - mapper works correctly regardless of properties names.

What I am talking about?

Let's consider custom XmlMapper:

private static final ObjectMapper XML_MAPPER = new XmlMapper()
            .setAnnotationIntrospector(
                    AnnotationIntrospector.pair(
                            new JaxbAnnotationIntrospector(),
                            new JacksonAnnotationIntrospector()
                    )
            )
            .registerModule(new JavaTimeModule())
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE);

and simple Data Transfer Object (DTO):

    @XmlRootElement(name = "person")
    @XmlAccessorType(XmlAccessType.NONE)
    static class Person {
        @XmlAttribute
        final int age;

        @XmlAttribute
        final String name;

        @XmlAttribute
        final LocalDate dateOfBirth;

        @ConstructorProperties({"age","name","dateOfBirth"})
        public Person(int age, String name, LocalDate dateOfBirth) {
            this.age = age;
            this.name = name;
            this.dateOfBirth = dateOfBirth;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    ", dateOfBirth=" + dateOfBirth +
                    '}';
        }
    }

I created test to reproduce the issue:

@Test
@DisplayName("Check xml deseralization for Person class")
void deserialization() throws IOException {
    String xml = "<person age=\"26\" name=\"Fred\" date-of-birth=\"1991-11-07\"/>";
    Person person = XML_MAPPER.readValue(xml, Person.class);
    Assertions.assertEquals("Person{age=26, name='Fred', dateOfBirth=1991-11-07}", person.toString());
}

It's strange for me why test is passed regardless of @ConstructorProperties annotation. The test passed with annotation

        @ConstructorProperties({"a","b","c"})
        public Person(int age, String name, LocalDate dateOfBirth) {
            this.age = age;
            this.name = name;
            this.dateOfBirth = dateOfBirth;
        }

Is it a magic? How jackson processes this annotation? What is an equivalent in jackson annotations to ConstructorProperties?


Solution

  • It's passing because the JaxbAnnotationIntrospector can determine the property names from the @XmlAttribute annotations.

    The doc on AnnotationIntrospectorPair says:

    Helper class that allows using 2 introspectors such that one introspector acts as the primary one to use; and second one as a fallback used if the primary does not provide conclusive or useful result for a method.

    The JacksonAnnotationIntrospector (which understands the @ConstructorProperties annotation) isn't being used at all.

    If you remove all the JAXB annotations your test will only pass when the correct names are specified in @ConstructorProperties.

    If you want to do it "the jackson way", then remove the JAXB annotations and the JaxbAnnotationIntrospector completely (just drop the call to setAnnotationIntrospector, the mapper will default to using the JacksonAnnotationIntrospector).

    Deserialisation will work, but you'll have to add some jackson native annotations if you want to achieve the same serialised form:

    @JacksonXmlRootElement(localName = "person")
    static class Person {
        @JacksonXmlProperty(isAttribute = true)
        final int age;
    
        @JacksonXmlProperty(isAttribute = true)
        final String name;
    
        @JacksonXmlProperty(isAttribute = true)
        final LocalDate dateOfBirth;
    
        @ConstructorProperties({"age", "name", "dateOfBirth"})
        public Person(int age, String name, LocalDate dateOfBirth) {
            this.age = age;
            this.name = name;
            this.dateOfBirth = dateOfBirth;
        }
    
        //...