FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
561 stars 221 forks source link

XML serialization of floating-point infinity is incompatible with JAXB and XML Schema #643

Closed ahcodedthat closed 3 months ago

ahcodedthat commented 3 months ago

As of version 2.16.1, infinite values of float and double are serialized in a way that is incompatible with the XML Schema definition and JAXB. Specifically, jackson-dataformat-xml serializes these values as the strings Infinity or -Infinity. XML Schema, however, says they should be serialized as INF or -INF, and that is what JAXB does.

Example program (click to show) ```java package org.example; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; public class Main { public static void main(String[] args) throws IOException { ExampleObject original, deserialized; String serialized; original = new ExampleObject(); original.x = Double.POSITIVE_INFINITY; original.y = Double.NEGATIVE_INFINITY; original.z = Double.NaN; original.fx = Float.POSITIVE_INFINITY; original.fy = Float.NEGATIVE_INFINITY; original.fz = Float.NaN; System.out.println("--- Jackson serialization ---"); serialized = serializeWithJackson(original); System.out.println(serialized); System.out.println("--- Jackson deserialization ---"); deserialized = deserializeWithJackson(serialized); System.out.println(deserialized); System.out.println("--- JAXB serialization ---"); serialized = serializeWithJaxb(original); System.out.println(serialized); System.out.println("--- JAXB deserialization ---"); deserialized = deserializeWithJaxb(serialized); System.out.println(deserialized); System.out.println("--- serialized with JAXB, deserialized with Jackson ---"); deserialized = deserializeWithJackson(serialized); System.out.println(deserialized); System.out.println("--- serialized with Jackson, deserialized with JAXB ---"); serialized = serializeWithJackson(original); deserialized = deserializeWithJaxb(serialized); System.out.println(deserialized); } private static String serializeWithJackson(ExampleObject object) throws IOException { var buf = new StringWriter(); new XmlMapper().writeValue(buf, object); return buf.toString(); } private static ExampleObject deserializeWithJackson(String xml) throws JsonProcessingException { return new XmlMapper().readValue(xml, ExampleObject.class); } private static String serializeWithJaxb(ExampleObject object) { var buf = new StringWriter(); JAXB.marshal(object, buf); return buf.toString(); } private static ExampleObject deserializeWithJaxb(String xml) { return JAXB.unmarshal(new StringReader(xml), ExampleObject.class); } } @XmlRootElement(name = "example") class ExampleObject { @XmlElement public double x, y, z; @XmlElement public float fx, fy, fz; @Override public String toString() { return String.format("x=%f y=%f z=%f fx=%f fy=%f fz=%f", x, y, z, fx, fy, fz); } } ```
Maven POM for example program (click to show) ```xml 4.0.0 org.example jackson-xml-double 1.0-SNAPSHOT 17 17 UTF-8 com.fasterxml.jackson.core jackson-databind 2.16.1 com.fasterxml.jackson.core jackson-annotations 2.16.1 com.fasterxml.jackson.dataformat jackson-dataformat-xml 2.16.1 javax.xml.bind jaxb-api 2.3.0 org.glassfish.jaxb jaxb-runtime 2.3.3 ```
Output from example program (click to show) ``` --- Jackson serialization --- Infinity-InfinityNaNInfinity-InfinityNaN --- Jackson deserialization --- x=Infinity y=-Infinity z=NaN fx=Infinity fy=-Infinity fz=NaN --- JAXB serialization --- INF -INF NaN INF -INF NaN --- JAXB deserialization --- x=Infinity y=-Infinity z=NaN fx=Infinity fy=-Infinity fz=NaN --- serialized with JAXB, deserialized with Jackson --- x=Infinity y=-Infinity z=NaN fx=Infinity fy=-Infinity fz=NaN --- serialized with Jackson, deserialized with JAXB --- x=0.000000 y=0.000000 z=NaN fx=0.000000 fy=0.000000 fz=NaN ```

As the example program's output shows, Jackson understands both its own format and the XML Schema format for floating-point infinity. JAXB, however, understands only the XML Schema format, and fails to parse Jackson's format.

The problem seems to be that jackson-dataformat-xml calls TypedXMLStreamWriter methods to serialize floating-point values, which ultimately uses NumberUtil.write{Float,Double} from StAX2, which in turn uses java.lang.String.valueOf to serialize the number, without any special handling of infinity.

Deserialization of XML Schema-formatted numbers seems to work correctly. Only serialization has an issue.

This issue only affects positive and negative infinity. java.lang.String.valueOf differs from XML Schema only in how it represents infinity; it uses the same format as XML Schema for NaN and finite values.

cowtowncoder commented 3 months ago

Jackson XML module is neither JAXB implementation, nor make any use of XML Schema. So in that sense expected behavior is not necessarily same.

Having said that, if anyone has time to come up with a PR I'd be happy to help get that merged -- the only (?) requirement would be to have new ToXMLGenerator.Feature for enabling different serialization: this is needed for backwards compatibility.

ahcodedthat commented 3 months ago

Ok. I've submitted PR #644.