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

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

Closed
ahcodedthat opened this issue Mar 8, 2024 · 2 comments · Fixed by #644
Closed
Labels
2.17 Issues planned at earliest for 2.17
Milestone

Comments

@ahcodedthat
Copy link
Contributor

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)
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 version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>org.example</groupId>
	<artifactId>jackson-xml-double</artifactId>
	<version>1.0-SNAPSHOT</version>

	<properties>
		<maven.compiler.source>17</maven.compiler.source>
		<maven.compiler.target>17</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<dependencies>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.16.1</version>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>2.16.1</version>
		</dependency>

		<dependency>
			<groupId>com.fasterxml.jackson.dataformat</groupId>
			<artifactId>jackson-dataformat-xml</artifactId>
			<version>2.16.1</version>
		</dependency>

		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.3.0</version>
		</dependency>

		<dependency>
			<groupId>org.glassfish.jaxb</groupId>
			<artifactId>jaxb-runtime</artifactId>
			<version>2.3.3</version>
		</dependency>
	</dependencies>
</project>
Output from example program (click to show)
--- Jackson serialization ---
<ExampleObject><x>Infinity</x><y>-Infinity</y><z>NaN</z><fx>Infinity</fx><fy>-Infinity</fy><fz>NaN</fz></ExampleObject>
--- Jackson deserialization ---
x=Infinity y=-Infinity z=NaN fx=Infinity fy=-Infinity fz=NaN
--- JAXB serialization ---
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<example>
    <x>INF</x>
    <y>-INF</y>
    <z>NaN</z>
    <fx>INF</fx>
    <fy>-INF</fy>
    <fz>NaN</fz>
</example>

--- 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
Copy link
Member

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 added a commit to ahcodedthat/jackson-dataformat-xml that referenced this issue Mar 8, 2024
ahcodedthat added a commit to ahcodedthat/jackson-dataformat-xml that referenced this issue Mar 8, 2024
@ahcodedthat
Copy link
Contributor Author

Ok. I've submitted PR #644.

cowtowncoder pushed a commit that referenced this issue Mar 13, 2024
@cowtowncoder cowtowncoder added the 2.17 Issues planned at earliest for 2.17 label Mar 13, 2024
@cowtowncoder cowtowncoder added this to the 2.17.0 milestone Mar 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.17 Issues planned at earliest for 2.17
Projects
None yet
2 participants