Skip to content

Commit be5e719

Browse files
authored
Fix #643: Add ToXmlGenerator.Feature or allowing XML Schema/JAXB compatible Infinity representation (#644)
1 parent e80909f commit be5e719

File tree

4 files changed

+133
-9
lines changed

4 files changed

+133
-9
lines changed

release-notes/CREDITS-2.x

+6
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,9 @@ Arthur Chan (@arthurscchan)
249249
* Reported, contributed fix for #618: `ArrayIndexOutOfBoundsException` thrown for invalid
250250
ending XML string when using JDK default Stax XML parser
251251
(2.17.0)
252+
253+
Alex H (@ahcodedthat)
254+
255+
* Contribtued #643: XML serialization of floating-point infinity is incompatible
256+
with JAXB and XML Schema
257+
(2.17.0)

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Project: jackson-dataformat-xml
1818
(FromXmlParser.Feature.AUTO_DETECT_XSI_TYPE)
1919
#637: `JacksonXmlAnnotationIntrospector.findNamespace()` should
2020
properly merge namespace information
21+
#643: XML serialization of floating-point infinity is incompatible
22+
with JAXB and XML Schema
23+
(contributed by Alex H)
2124
* Upgrade Woodstox to 6.6.1 (latest at the time)
2225

2326
2.16.2 (09-Mar-2024)

src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java

+41
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,37 @@ public enum Feature implements FormatFeature
106106
* @since 2.17
107107
*/
108108
AUTO_DETECT_XSI_TYPE(false),
109+
110+
/**
111+
* Feature that determines how floating-point infinity values are
112+
* serialized.
113+
*<p>
114+
* By default, {@link Float#POSITIVE_INFINITY} and
115+
* {@link Double#POSITIVE_INFINITY} are serialized as {@code Infinity},
116+
* and {@link Float#NEGATIVE_INFINITY} and
117+
* {@link Double#NEGATIVE_INFINITY} are serialized as
118+
* {@code -Infinity}. This is the representation that Java normally
119+
* uses for these values (see {@link Float#toString(float)} and
120+
* {@link Double#toString(double)}), but JAXB and other XML
121+
* Schema-conforming readers won't understand it.
122+
*<p>
123+
* With this feature enabled, these values are instead serialized as
124+
* {@code INF} and {@code -INF}, respectively. This is the
125+
* representation that XML Schema and JAXB use (see the XML Schema
126+
* primitive types
127+
* <a href="https://www.w3.org/TR/xmlschema-2/#float"><code>float</code></a>
128+
* and
129+
* <a href="https://www.w3.org/TR/xmlschema-2/#double"><code>double</code></a>).
130+
*<p>
131+
* When deserializing, Jackson always understands both representations,
132+
* so there is no corresponding
133+
* {@link com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser.Feature}.
134+
*<p>
135+
* Feature is disabled by default for backwards compatibility.
136+
*
137+
* @since 2.17
138+
*/
139+
WRITE_XML_SCHEMA_CONFORMING_FLOATS(false),
109140
;
110141

111142
final boolean _defaultState;
@@ -1174,6 +1205,11 @@ public void writeNumber(long l) throws IOException
11741205
@Override
11751206
public void writeNumber(double d) throws IOException
11761207
{
1208+
if (Double.isInfinite(d) && isEnabled(Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)) {
1209+
writeNumber(d > 0d ? "INF" : "-INF");
1210+
return;
1211+
}
1212+
11771213
_verifyValueWrite("write number");
11781214
if (_nextName == null) {
11791215
handleMissingName();
@@ -1202,6 +1238,11 @@ public void writeNumber(double d) throws IOException
12021238
@Override
12031239
public void writeNumber(float f) throws IOException
12041240
{
1241+
if (Float.isInfinite(f) && isEnabled(Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)) {
1242+
writeNumber(f > 0f ? "INF" : "-INF");
1243+
return;
1244+
}
1245+
12051246
_verifyValueWrite("write number");
12061247
if (_nextName == null) {
12071248
handleMissingName();

src/test/java/com/fasterxml/jackson/dataformat/xml/ser/TestSerialization.java

+83-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.fasterxml.jackson.dataformat.xml.ser;
22

3-
import java.io.*;
43
import java.util.*;
54

65
import com.fasterxml.jackson.annotation.JsonProperty;
6+
77
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
88
import com.fasterxml.jackson.dataformat.xml.XmlTestBase;
99
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData;
@@ -31,6 +31,22 @@ static class AttrAndElem
3131
public int attr = 42;
3232
}
3333

34+
static class Floats
35+
{
36+
public float elem;
37+
38+
@JacksonXmlProperty(isAttribute=true, localName="attr")
39+
public float attr;
40+
}
41+
42+
static class Doubles
43+
{
44+
public double elem;
45+
46+
@JacksonXmlProperty(isAttribute=true, localName="attr")
47+
public double attr;
48+
}
49+
3450
static class WrapperBean<T>
3551
{
3652
public T value;
@@ -81,37 +97,37 @@ static class CustomMap extends LinkedHashMap<String, Integer> { }
8197

8298
private final XmlMapper _xmlMapper = new XmlMapper();
8399

84-
public void testSimpleAttribute() throws IOException
100+
public void testSimpleAttribute() throws Exception
85101
{
86102
String xml = _xmlMapper.writeValueAsString(new AttributeBean());
87103
xml = removeSjsxpNamespace(xml);
88104
assertEquals("<AttributeBean attr=\"something\"/>", xml);
89105
}
90106

91-
public void testSimpleNsElem() throws IOException
107+
public void testSimpleNsElem() throws Exception
92108
{
93109
String xml = _xmlMapper.writeValueAsString(new NsElemBean());
94110
xml = removeSjsxpNamespace(xml);
95111
// here we assume woodstox automatic prefixes, not very robust but:
96112
assertEquals("<NsElemBean><wstxns1:text xmlns:wstxns1=\"http://foo\">blah</wstxns1:text></NsElemBean>", xml);
97113
}
98114

99-
public void testSimpleNsElemWithJsonProp() throws IOException
115+
public void testSimpleNsElemWithJsonProp() throws Exception
100116
{
101117
String xml = _xmlMapper.writeValueAsString(new NsElemBean2());
102118
xml = removeSjsxpNamespace(xml);
103119
// here we assume woodstox automatic prefixes, not very robust but:
104120
assertEquals("<NsElemBean2><wstxns1:text xmlns:wstxns1=\"http://foo\">blah</wstxns1:text></NsElemBean2>", xml);
105121
}
106122

107-
public void testSimpleAttrAndElem() throws IOException
123+
public void testSimpleAttrAndElem() throws Exception
108124
{
109125
String xml = _xmlMapper.writeValueAsString(new AttrAndElem());
110126
xml = removeSjsxpNamespace(xml);
111127
assertEquals("<AttrAndElem id=\"42\"><elem>whatever</elem></AttrAndElem>", xml);
112128
}
113129

114-
public void testMap() throws IOException
130+
public void testMap() throws Exception
115131
{
116132
// First, map in a general wrapper
117133
LinkedHashMap<String,Integer> map = new LinkedHashMap<String,Integer>();
@@ -136,7 +152,7 @@ public void testMap() throws IOException
136152
xml);
137153
}
138154

139-
public void testNakedMap() throws IOException
155+
public void testNakedMap() throws Exception
140156
{
141157
CustomMap input = new CustomMap();
142158
input.put("a", 123);
@@ -152,14 +168,14 @@ public void testNakedMap() throws IOException
152168
assertEquals(Integer.valueOf(456), result.get("b"));
153169
}
154170

155-
public void testCDataString() throws IOException
171+
public void testCDataString() throws Exception
156172
{
157173
String xml = _xmlMapper.writeValueAsString(new CDataStringBean());
158174
xml = removeSjsxpNamespace(xml);
159175
assertEquals("<CDataStringBean><value><![CDATA[<some<data\"]]></value></CDataStringBean>", xml);
160176
}
161177

162-
public void testCDataStringArray() throws IOException
178+
public void testCDataStringArray() throws Exception
163179
{
164180
String xml = _xmlMapper.writeValueAsString(new CDataStringArrayBean());
165181
xml = removeSjsxpNamespace(xml);
@@ -175,4 +191,62 @@ public void testJAXB() throws Exception
175191
System.out.println("JAXB -> "+sw);
176192
}
177193
*/
194+
195+
public void testFloatInfinity() throws Exception
196+
{
197+
Floats infinite = new Floats();
198+
infinite.attr = Float.POSITIVE_INFINITY;
199+
infinite.elem = Float.NEGATIVE_INFINITY;
200+
201+
Floats finite = new Floats();
202+
finite.attr = 42.5f;
203+
finite.elem = 1337.875f;
204+
205+
checkFloatInfinity(infinite, false, "<Floats attr=\"Infinity\"><elem>-Infinity</elem></Floats>");
206+
checkFloatInfinity(finite, false, "<Floats attr=\"42.5\"><elem>1337.875</elem></Floats>");
207+
checkFloatInfinity(infinite, true, "<Floats attr=\"INF\"><elem>-INF</elem></Floats>");
208+
checkFloatInfinity(finite, true, "<Floats attr=\"42.5\"><elem>1337.875</elem></Floats>");
209+
}
210+
211+
private void checkFloatInfinity(Floats original, boolean xmlSchemaConforming, String expectedXml) throws Exception
212+
{
213+
_xmlMapper.configure(ToXmlGenerator.Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming);
214+
215+
String xml = _xmlMapper.writeValueAsString(original);
216+
xml = removeSjsxpNamespace(xml);
217+
assertEquals(expectedXml, xml);
218+
219+
Floats deserialized = _xmlMapper.readValue(xml, Floats.class);
220+
assertEquals(original.attr, deserialized.attr);
221+
assertEquals(original.elem, deserialized.elem);
222+
}
223+
224+
public void testDoubleInfinity() throws Exception
225+
{
226+
Doubles infinite = new Doubles();
227+
infinite.attr = Double.POSITIVE_INFINITY;
228+
infinite.elem = Double.NEGATIVE_INFINITY;
229+
230+
Doubles finite = new Doubles();
231+
finite.attr = 42.5d;
232+
finite.elem = 1337.875d;
233+
234+
checkDoubleInfinity(infinite, false, "<Doubles attr=\"Infinity\"><elem>-Infinity</elem></Doubles>");
235+
checkDoubleInfinity(finite, false, "<Doubles attr=\"42.5\"><elem>1337.875</elem></Doubles>");
236+
checkDoubleInfinity(infinite, true, "<Doubles attr=\"INF\"><elem>-INF</elem></Doubles>");
237+
checkDoubleInfinity(finite, true, "<Doubles attr=\"42.5\"><elem>1337.875</elem></Doubles>");
238+
}
239+
240+
private void checkDoubleInfinity(Doubles original, boolean xmlSchemaConforming, String expectedXml) throws Exception
241+
{
242+
_xmlMapper.configure(ToXmlGenerator.Feature.WRITE_XML_SCHEMA_CONFORMING_FLOATS, xmlSchemaConforming);
243+
244+
String xml = _xmlMapper.writeValueAsString(original);
245+
xml = removeSjsxpNamespace(xml);
246+
assertEquals(expectedXml, xml);
247+
248+
Doubles deserialized = _xmlMapper.readValue(xml, Doubles.class);
249+
assertEquals(original.attr, deserialized.attr);
250+
assertEquals(original.elem, deserialized.elem);
251+
}
178252
}

0 commit comments

Comments
 (0)