FasterXML / jackson-modules-java8

Set of support modules for Java 8 datatypes (Optionals, date/time) and features (parameter names)
Apache License 2.0
398 stars 116 forks source link

Xml cannot read in Optional.empty #280

Closed HenryYihengXu closed 10 months ago

HenryYihengXu commented 11 months ago

I have an object

class MyObject {
  private final int a;
  public MyObject() {
    a = 1;
  }

  // Getter
}

then another object with an optional field of the object above

class MyObject2 {
  private final Optional<MyObject> myObject;
  public MyObject2() {
    myObject = Optional.empty();
  }
  // Getter
}

I serialize MyObject2 as

MyObject2 myObject2 = new MyObject2();
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.registerModule(new Jdk8Module());
xmlMapper.writeValue(new File("myObject2.xml"), myObject2);

I got

<MyObject2>
  <MyObject/>
</MyObject2>

I'm assuming a single <xxx/> denotes optional empty? Is this correct? Assume it is correct, I deserialize it as

MyObject2 deserializedMyObject2 =
                xmlMapper.readValue(new File("myObject2.xml"), MyObject2.class);

But then I found the deserialized one is not Optional.empty. It seems to call the default constructor of MyObject so it has a = 1 in it. Do you know how should I actually deserialize Optional.empty?

cowtowncoder commented 11 months ago

Intent with Jackson XML module is that it should be able to read what it writes: so the XML structure created with XmlMapper.writeValue(myObject2) should be readable with XmlMapper.readValue(xmlDoc, MyObject2.class). If not, that is considered bug.

Package does not necessarily allow mapping different kinds of XML representations.

HenryYihengXu commented 11 months ago

Then this should be a bug. I tried reading back what it writes. It's readable, but the deserialized object is different from the original one.

Here is the code:

    @Test
    void testJacksonXml2() throws IOException {
        MyObject2 myObject2 = new MyObject2();

        XmlMapper xmlMapper = new XmlMapper();
        xmlMapper.registerModule(new Jdk8Module());
        xmlMapper.writeValue(new File("test-jackson.xml"), myObject2);

        MyObject2 deserializedMyObject2 = xmlMapper.readValue(new File("test-jackson.xml"), MyObject2.class);
        xmlMapper.writeValue(new File("deserialized-test-jackson.xml"), deserializedMyObject2);
        assertThat(myObject2).isEqualTo(deserializedMyObject2);
    }

The test is failing. And I checked "test-jackson.xml" and "deserialized-test-jackson.xml". They are indeed different. The former is just

<MyObject2>
  <MyObject/>
</MyObject2>

but the later one is

<MyObject2>
  <myObject>
    <a>1</a>
  </myObject>
</MyObject2>

Could you fix this bug? I would appreciate it!

cowtowncoder commented 11 months ago

That sounds like a bug then.

Unfortunately I doubt I have time to tackle this issue at this point. But if you or anyone else has time and interest, I can help get PR reviewed and so on.

One practical challenge here is that of testing, as the issue requires both XML module and JDK 8 datatype module -- neither of which should have dependency to the other.

But I think same problem would affect use of AtomicReference type, too, and if reproducing the issue with it, test could be added in jackson-dataformat-xml. And fix, if any, would likely work for Optional too as their handling is very similar internally.

HenryYihengXu commented 11 months ago

Do you have an idea on how to fix it? I looked at your OptionalDeserializer, looks like it should deserialize to Optional.empty()?

cowtowncoder commented 11 months ago

A major problem is that empty XML element can mean either "empty" value (like "" for java.lang.String or, for POJOs, instance constructed with default constructor); OR null value. In this case I think you get "empty" Object, then wrapped in Optional.

One way to force "null-ness" is to add xsi:nil attribute like so:

<!-- NOTE: xmlns:xsi could be declared in parent to avoid using multiple times
   -->
<MyObject2>
  <MyObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" />
</MyObject2>
cowtowncoder commented 10 months ago

Ok, so, above xsi:nil works. But to produce it on writing, you need to enable

ToXmlGenerator.Feature.WRITE_NULLS_AS_XSI_NIL

after which things work.

I don't see a working way to solve this problem in other way but I suggest you try this.