Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When the localName property of @JacksonXmlProperty is not set, the required of @JsonProperty does not detect the property name of POJO #628

Closed
linghengqian opened this issue Jan 24, 2024 · 10 comments

Comments

@linghengqian
Copy link

linghengqian commented Jan 24, 2024

TL;

  • When the localName property of @JacksonXmlProperty is not set, the required of @JsonProperty does not detect the property name of POJO.
  • I have an example to verify this phenomenon. I don't seem to see this documented in the documentation. This unit testing and reproduction process is located at https://github.com/linghengqian/jackson-mixed-annotations-test .
public class RequiredTest {
    @Test
    void assertRequiredValue() {
        ObjectMapper XML_MAPPER = XmlMapper.builder().build();
        Assertions.assertDoesNotThrow(() -> {
            XML_MAPPER.readValue("""
                    <Record type="">
                        <driverClassName>
                        </driverClassName>
                    </Record>
                    """, TestRecord.class);
        });
        Assertions.assertDoesNotThrow(() -> {
            XML_MAPPER.readValue("""
                    <Record type="">
                        <driverClassName>
                        </driverClassName>
                    </Record>
                    """, TestRecordWithoutLocalName.class);
        });
    }

    @Getter
    @NoArgsConstructor
    @JacksonXmlRootElement(localName = "Record")
    static class TestRecord {
        private String type;

        private String driverClassName;

        @JsonCreator
        public TestRecord(
                @JsonProperty(required = true) @JacksonXmlProperty(localName = "type", isAttribute = true)
                String type,
                @JsonProperty(required = true) @JacksonXmlProperty(localName = "driverClassName")
                String driverClassName) {
            this.type = type;
            this.driverClassName = driverClassName;
        }
    }

    @Getter
    @NoArgsConstructor
    @JacksonXmlRootElement(localName = "Record")
    static class TestRecordWithoutLocalName {
        private String type;

        private String driverClassName;

        @JsonCreator
        public TestRecordWithoutLocalName(
                @JsonProperty(required = true) @JacksonXmlProperty(isAttribute = true)
                String type,
                @JsonProperty(required = true) @JacksonXmlProperty(localName = "driverClassName")
                String driverClassName) {
            this.type = type;
            this.driverClassName = driverClassName;
        }
    }
}
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.273 s <<< FAILURE! -- in com.lingh.RequiredTest
[ERROR] com.lingh.RequiredTest.assertRequiredValue -- Time elapsed: 0.243 s <<< FAILURE!
org.opentest4j.AssertionFailedError: 
Unexpected exception thrown: com.fasterxml.jackson.databind.exc.MismatchedInputException: Missing required creator property '' (index 0)
 at [Source: (StringReader); line: 4, column: 1] (through reference chain: com.lingh.RequiredTest$TestRecordWithoutLocalName[""])
        at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
        at org.junit.jupiter.api.AssertDoesNotThrow.createAssertionFailedError(AssertDoesNotThrow.java:84)
        at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:53)
        at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:36)
        at org.junit.jupiter.api.Assertions.assertDoesNotThrow(Assertions.java:3168)
        at com.lingh.RequiredTest.assertRequiredValue(RequiredTest.java:26)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Missing required creator property '' (index 0)
 at [Source: (StringReader); line: 4, column: 1] (through reference chain: com.lingh.RequiredTest$TestRecordWithoutLocalName[""])
        at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
        at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1781)
        at com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer._findMissing(PropertyValueBuffer.java:192)
        at com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer.getParameters(PropertyValueBuffer.java:158)
        at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromObjectWith(ValueInstantiator.java:301)
        at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:202)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:526)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
        at com.fasterxml.jackson.dataformat.xml.deser.XmlDeserializationContext.readRootValue(XmlDeserializationContext.java:104)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4899)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3846)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3814)
        at com.lingh.RequiredTest.lambda$assertRequiredValue$1(RequiredTest.java:27)
        at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:49)
        ... 6 more
@cowtowncoder
Copy link
Member

Ok the problem here is, I think, that for naming purposes @JsonProperty and @JacksonXmlProperty are sort of synonyms. And specifically so that one with higher precedence (as they are detected by different AnnotationIntrospectors, I think by default is @JacksonXmlProperty) will be used for all name aspects -- both namespace and local name.
So in this case it'd be as if @JsonProperty did not even exist.

I think this should be fixable, but has to be done in jackson-databind AnnotationIntrospectorPair which handles merging of annotation information from different source: method findNameForDeserialization() (and findNameForSerialization()) at least would need to be changed a bit.

@cowtowncoder
Copy link
Member

Actually, I take that back: constructor parameters' names are only discovered if using jackson-module-parameter-names (from https://github.com/FasterXML/jackson-modules-java8/).
So this:

                @JsonProperty(required = true) @JacksonXmlProperty(isAttribute = true)
                String type,

Does not have name available: either @JsonProperty(value = "type") or @JacksonXmlProperty(localName="type") would indeed be needed.

@linghengqian
Copy link
Author

org.opentest4j.AssertionFailedError: Unexpected exception thrown: com.fasterxml.jackson.databind.exc.MismatchedInputException: Missing required creator property '' (index 0)
 at [Source: (StringReader); line: 4, column: 1] (through reference chain: com.lingh.RequiredTest$TestRecordWithoutLocalName[""])

@cowtowncoder
Copy link
Member

cowtowncoder commented Feb 3, 2024

@linghengqian Ok. In that case, that's problematic. I'll need to figure out if Parameter names module (or something in. Jackson databind) needs improvement -- ideally missing/empty local name should not override "implicit" name brought in by module.

I do have a related fix to make, via FasterXML/jackson-databind#4364 but not sure that is enough.
I also have another fix to make in this module (see #637), but fundamentally something else might be needed.

EDIT: now both fixes are in so test case might have a chance to work -- but I have not tested full set up yet (test needs to be in jackson-modules-java8 repo to find parameter names).

EDIT 2: tested against jackson-parameter-names -- FasterXML/jackson-modules-java8#301 -- and things appear to work.

@cowtowncoder
Copy link
Member

@linghengqian If you have a way to test against 2.17.0-SNAPSHOT the issue may have been resolved.
I would be interested if this is (or is not) the case, with fixes I have made.

@linghengqian
Copy link
Author

  1. 9f1aa0f
  2. FasterXML/jackson-modules-java8@1876ea8
  • This slightly changes the experimental procedure
sdk install java 21.0.2-graalce
sdk use java 21.0.2-graalce

git clone [email protected]:FasterXML/jackson-dataformat-xml.git -b 2.17
cd ./jackson-dataformat-xml/
git reset --hard 9f1aa0f5054a7a75acfc8155df9aee50059fb5da
./mvnw clean install -T1C -DskipTests

cd ../

git clone [email protected]:FasterXML/jackson-modules-java8.git -b 2.17
cd ./jackson-modules-java8/
git reset --hard 1876ea82f326e05757d2a439af2b56290bb5d7c3
./mvnw clean install -T1C -DskipTests

cd ../

git clone [email protected]:linghengqian/jackson-mixed-annotations-test.git
cd ./jackson-mixed-annotations-test/
./mvnw clean test
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Missing required creator property '' (index 0)
 at [Source: (StringReader); line: 4, column: 1] (through reference chain: com.lingh.RequiredTest$TestRecordWithoutLocalName[""])
        at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
        at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1781)
        at com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer._findMissing(PropertyValueBuffer.java:192)
        at com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer.getParameters(PropertyValueBuffer.java:158)
        at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromObjectWith(ValueInstantiator.java:301)
        at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:202)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:526)
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
        at com.fasterxml.jackson.dataformat.xml.deser.XmlDeserializationContext.readRootValue(XmlDeserializationContext.java:104)
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4905)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3848)
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3816)
        at com.lingh.RequiredTest.lambda$assertRequiredValue$1(RequiredTest.java:28)
        at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:49)
        ... 6 more
  • Am I missing something?

@cowtowncoder
Copy link
Member

That sounds like problem still persists: I can reproduce this with your project as well.

Odd part is that it really looks as if ParameterNamesModule was not working: if I comment out @JsonProperty and @JacksonXmlProperty annotations problem still persists (wrt no name found for constructor parameter).
(I also temporarily removed Lombok annotations to ensure it had nothing to do with the issue).
I'll see if I can figure out why this might be happening.

cowtowncoder added a commit that referenced this issue Feb 5, 2024
@cowtowncoder
Copy link
Member

Ok. I think I know the problem: project settings are such that javac will not actually include method/constructor parameter names in bytecode -- and as such ParameterNamesModule cannot find any.

I can make test pass by adding this in pom.xml:

    <build>
      <plugins>
       <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <inherited>true</inherited>
        <configuration>
          <compilerArgs>
            <arg>-Xlint</arg>
            <arg>-parameters</arg>
          </compilerArgs>
        </configuration>
       </plugin>
      </plugins>
    </build>

@linghengqian
Copy link
Author

linghengqian commented Feb 5, 2024

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

class Person must be compiled with Java 8 compliant compiler with option to store formal parameter names turned on (-parameters option). For more information about Java 8 API for accessing parameter names at runtime see this

@cowtowncoder
Copy link
Member

@linghengqian Yes, let's close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants