Skip to content

Commit df30e79

Browse files
committed
Merge branch '2.19'
2 parents 8b9f7d3 + 70511f0 commit df30e79

File tree

6 files changed

+222
-5
lines changed

6 files changed

+222
-5
lines changed

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Project: jackson-databind
3030
Map object is ignored when Map key type not defined
3131
(reported by @devdanylo)
3232
(fix by Joo-Hyuk K)
33+
#4771: `QName` (de)serialization ignores prefix
34+
(reported by @jpraet)
35+
(fix contributed by @mcvayc)
3336
#4772: Serialization and deserialization issue of sub-types used with
3437
`JsonTypeInfo.Id.DEDUCTION` where sub-types are Object and Array
3538
(reported by Eduard G)

src/main/java/tools/jackson/databind/ext/CoreXMLDeserializers.java

+36-2
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,50 @@ public Std(Class<?> raw, int kind) {
8484
public Object deserialize(JsonParser p, DeserializationContext ctxt)
8585
throws JacksonException
8686
{
87-
// For most types, use super impl; but GregorianCalendar also allows
88-
// integer value (timestamp), which needs separate handling
87+
// GregorianCalendar also allows integer value (timestamp),
88+
// which needs separate handling
8989
if (_kind == TYPE_G_CALENDAR) {
9090
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
9191
return _gregorianFromDate(ctxt, _parseDate(p, ctxt));
9292
}
9393
}
94+
// QName also allows object value, which needs separate handling
95+
if (_kind == TYPE_QNAME) {
96+
if (p.hasToken(JsonToken.START_OBJECT)) {
97+
return _parseQNameObject(p, ctxt);
98+
}
99+
}
94100
return super.deserialize(p, ctxt);
95101
}
96102

103+
private QName _parseQNameObject(JsonParser p, DeserializationContext ctxt)
104+
throws JacksonException
105+
{
106+
JsonNode tree = ctxt.readTree(p);
107+
108+
JsonNode localPart = tree.get("localPart");
109+
if (localPart == null) {
110+
ctxt.reportInputMismatch(this,
111+
"Object value for `QName` is missing required property 'localPart'");
112+
}
113+
114+
if (!localPart.isString()) {
115+
ctxt.reportInputMismatch(this,
116+
"Object value property 'localPart' for `QName` must be of type STRING, not %s",
117+
localPart.getNodeType());
118+
}
119+
120+
JsonNode namespaceURI = tree.get("namespaceURI");
121+
if (namespaceURI != null) {
122+
if (tree.has("prefix")) {
123+
JsonNode prefix = tree.get("prefix");
124+
return new QName(namespaceURI.asString(), localPart.asString(), prefix.asString());
125+
}
126+
return new QName(namespaceURI.asString(), localPart.asString());
127+
}
128+
return new QName(localPart.asString());
129+
}
130+
97131
@Override
98132
protected Object _deserialize(String value, DeserializationContext ctxt)
99133
throws JacksonException

src/main/java/tools/jackson/databind/ext/OptionalHandlerFactory.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ public ValueSerializer<?> findSerializer(SerializationConfig config, JavaType ty
4444
}
4545

4646
String className = rawType.getName();
47-
if (className.startsWith(PACKAGE_PREFIX_JAVAX_XML) || hasSuperClassStartingWith(rawType, PACKAGE_PREFIX_JAVAX_XML)) {
48-
if (Duration.class.isAssignableFrom(rawType) || QName.class.isAssignableFrom(rawType)) {
47+
if (className.startsWith(PACKAGE_PREFIX_JAVAX_XML)
48+
|| hasSuperClassStartingWith(rawType, PACKAGE_PREFIX_JAVAX_XML)) {
49+
if (Duration.class.isAssignableFrom(rawType)) {
4950
return ToStringSerializer.instance;
5051
}
52+
if (QName.class.isAssignableFrom(rawType)) {
53+
return QNameSerializer.instance;
54+
}
5155
if (XMLGregorianCalendar.class.isAssignableFrom(rawType)) {
5256
return XMLGregorianCalendarSerializer.instance;
5357
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package tools.jackson.databind.ext;
2+
3+
import javax.xml.namespace.QName;
4+
5+
import com.fasterxml.jackson.annotation.JsonFormat;
6+
7+
import tools.jackson.core.JsonGenerator;
8+
import tools.jackson.core.JsonToken;
9+
import tools.jackson.core.type.WritableTypeId;
10+
import tools.jackson.databind.*;
11+
import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
12+
import tools.jackson.databind.jsontype.TypeSerializer;
13+
import tools.jackson.databind.ser.std.StdSerializer;
14+
import tools.jackson.databind.ser.std.ToStringSerializer;
15+
16+
public class QNameSerializer
17+
extends StdSerializer<QName>
18+
{
19+
public final static ValueSerializer<?> instance = new QNameSerializer();
20+
21+
public QNameSerializer() {
22+
super(QName.class);
23+
}
24+
25+
@Override
26+
public ValueSerializer<?> createContextual(SerializationContext serializers, BeanProperty property)
27+
{
28+
JsonFormat.Value format = findFormatOverrides(serializers, property, handledType());
29+
if (format != null) {
30+
JsonFormat.Shape shape = format.getShape();
31+
if (shape == JsonFormat.Shape.OBJECT) {
32+
return this;
33+
}
34+
}
35+
return ToStringSerializer.instance;
36+
}
37+
38+
@Override
39+
public void serialize(QName value, JsonGenerator g, SerializationContext ctxt)
40+
{
41+
g.writeStartObject(value);
42+
serializeProperties(value, g, ctxt);
43+
g.writeEndObject();
44+
}
45+
46+
@Override
47+
public final void serializeWithType(QName value, JsonGenerator g, SerializationContext ctxt,
48+
TypeSerializer typeSer)
49+
{
50+
WritableTypeId typeIdDef = typeSer.writeTypePrefix(g,
51+
ctxt, typeSer.typeId(value, JsonToken.START_OBJECT));
52+
serializeProperties(value, g, ctxt);
53+
typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
54+
}
55+
56+
private void serializeProperties(QName value, JsonGenerator g, SerializationContext ctxt)
57+
{
58+
g.writeStringProperty("localPart", value.getLocalPart());
59+
if (!value.getNamespaceURI().isEmpty()) {
60+
g.writeStringProperty("namespaceURI", value.getNamespaceURI());
61+
}
62+
if (!value.getPrefix().isEmpty()) {
63+
g.writeStringProperty("prefix", value.getPrefix());
64+
}
65+
}
66+
67+
@Override
68+
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
69+
{
70+
/*JsonObjectFormatVisitor v =*/ visitor.expectObjectFormat(typeHint);
71+
// TODO: would need to visit properties too, see `BeanSerializerBase`
72+
}
73+
}

src/test/java/tools/jackson/databind/ext/xml/MiscJavaXMLTypesReadWriteTest.java

+49-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import javax.xml.namespace.QName;
55
import org.junit.jupiter.api.Test;
66

7+
import com.fasterxml.jackson.annotation.JsonFormat;
8+
79
import tools.jackson.core.json.JsonWriteFeature;
10+
811
import tools.jackson.databind.*;
12+
import tools.jackson.databind.exc.MismatchedInputException;
913
import tools.jackson.databind.testutil.DatabindTestUtil;
1014
import tools.jackson.databind.testutil.NoCheckSubTypeValidator;
1115

@@ -33,7 +37,7 @@ public class MiscJavaXMLTypesReadWriteTest
3337
.build();
3438

3539
@Test
36-
public void testQNameSer() throws Exception
40+
public void testQNameSerDefault() throws Exception
3741
{
3842
QName qn = new QName("http://abc", "tag", "prefix");
3943
assertEquals(q(qn.toString()),
@@ -42,6 +46,19 @@ public void testQNameSer() throws Exception
4246
.writeValueAsString(qn));
4347
}
4448

49+
@Test
50+
public void testQNameSerToObject() throws Exception
51+
{
52+
QName qn = new QName("http://abc", "tag", "prefix");
53+
54+
ObjectMapper mapper = jsonMapperBuilder()
55+
.withConfigOverride(QName.class, cfg -> cfg.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT)))
56+
.disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES)
57+
.build();
58+
59+
assertEquals(a2q("{'localPart':'tag','namespaceURI':'http://abc','prefix':'prefix'}"), mapper.writeValueAsString(qn));
60+
}
61+
4562
@Test
4663
public void testDurationSer() throws Exception
4764
{
@@ -111,6 +128,37 @@ public void testQNameDeser() throws Exception
111128
assertEquals("", qn.getLocalPart());
112129
}
113130

131+
@Test
132+
public void testQNameDeserFromObject() throws Exception
133+
{
134+
String qstr = a2q("{'namespaceURI':'http://abc','localPart':'tag','prefix':'prefix'}");
135+
// Ok to read with standard ObjectMapper, no `@JsonFormat` needed
136+
QName qn = MAPPER.readValue(qstr, QName.class);
137+
138+
assertEquals("http://abc", qn.getNamespaceURI());
139+
assertEquals("tag", qn.getLocalPart());
140+
assertEquals("prefix", qn.getPrefix());
141+
}
142+
143+
@Test
144+
public void testQNameDeserFail() throws Exception
145+
{
146+
try {
147+
MAPPER.readValue("{}", QName.class);
148+
fail("Should not pass");
149+
} catch (MismatchedInputException e) {
150+
verifyException(e, "Object value for `QName` is missing required property 'localPart'");
151+
}
152+
153+
try {
154+
MAPPER.readValue(a2q("{'localPart': 123}"), QName.class);
155+
fail("Should not pass");
156+
} catch (MismatchedInputException e) {
157+
verifyException(e, "Object value property 'localPart'");
158+
verifyException(e, "must be of type STRING, not NUMBER");
159+
}
160+
}
161+
114162
@Test
115163
public void testXMLGregorianCalendarDeser() throws Exception
116164
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package tools.jackson.databind.ext.xml;
2+
3+
import java.util.stream.Stream;
4+
import javax.xml.namespace.QName;
5+
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.Arguments;
8+
import org.junit.jupiter.params.provider.MethodSource;
9+
10+
import com.fasterxml.jackson.annotation.JsonFormat;
11+
12+
import tools.jackson.databind.ObjectMapper;
13+
import tools.jackson.databind.testutil.DatabindTestUtil;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
17+
class QNameAsObjectReadWrite4771Test extends DatabindTestUtil
18+
{
19+
private final ObjectMapper MAPPER = newJsonMapper();
20+
21+
static class BeanWithQName {
22+
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
23+
public QName qname;
24+
25+
BeanWithQName() { }
26+
27+
public BeanWithQName(QName qName) {
28+
this.qname = qName;
29+
}
30+
}
31+
32+
@ParameterizedTest
33+
@MethodSource("provideAllPerumtationsOfQNameConstructor")
34+
void testQNameWithObjectSerialization(QName originalQName) throws Exception
35+
{
36+
BeanWithQName bean = new BeanWithQName(originalQName);
37+
38+
String json = MAPPER.writeValueAsString(bean);
39+
40+
QName deserializedQName = MAPPER.readValue(json, BeanWithQName.class).qname;
41+
42+
assertEquals(originalQName.getLocalPart(), deserializedQName.getLocalPart());
43+
assertEquals(originalQName.getNamespaceURI(), deserializedQName.getNamespaceURI());
44+
assertEquals(originalQName.getPrefix(), deserializedQName.getPrefix());
45+
}
46+
47+
static Stream<Arguments> provideAllPerumtationsOfQNameConstructor()
48+
{
49+
return Stream.of(
50+
Arguments.of(new QName("test-local-part")),
51+
Arguments.of(new QName("test-namespace-uri", "test-local-part")),
52+
Arguments.of(new QName("test-namespace-uri", "test-local-part", "test-prefix"))
53+
);
54+
}
55+
}

0 commit comments

Comments
 (0)