Skip to content

Commit 82a4618

Browse files
committed
Support OBJECT shape for QNAME serialization and deserialization.
This commit fixes #4771 by supporting the JsonFormat OBJECT shape during serialization and deserialization.
1 parent dd929e2 commit 82a4618

File tree

4 files changed

+196
-33
lines changed

4 files changed

+196
-33
lines changed

src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLDeserializers.java

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
* JDK 1.5. Types are directly needed by JAXB, but may be unavailable on some
1717
* limited platforms; hence separate out from basic deserializer factory.
1818
*/
19-
public class CoreXMLDeserializers extends Deserializers.Base
20-
{
19+
public class CoreXMLDeserializers extends Deserializers.Base {
2120
protected final static QName EMPTY_QNAME = QName.valueOf("");
2221

2322
/**
@@ -26,6 +25,7 @@ public class CoreXMLDeserializers extends Deserializers.Base
2625
* introspection) can be expensive we better reuse the instance.
2726
*/
2827
final static DatatypeFactory _dataTypeFactory;
28+
2929
static {
3030
try {
3131
_dataTypeFactory = DatatypeFactory.newInstance();
@@ -36,8 +36,7 @@ public class CoreXMLDeserializers extends Deserializers.Base
3636

3737
@Override
3838
public JsonDeserializer<?> findBeanDeserializer(JavaType type,
39-
DeserializationConfig config, BeanDescription beanDesc)
40-
{
39+
DeserializationConfig config, BeanDescription beanDesc) {
4140
Class<?> raw = type.getRawClass();
4241
if (raw == QName.class) {
4342
return new Std(raw, TYPE_QNAME);
@@ -77,8 +76,7 @@ public boolean hasDeserializerFor(DeserializationConfig config, Class<?> valueTy
7776
*
7877
* @since 2.4
7978
*/
80-
public static class Std extends FromStringDeserializer<Object>
81-
{
79+
public static class Std extends FromStringDeserializer<Object> {
8280
private static final long serialVersionUID = 1L;
8381

8482
protected final int _kind;
@@ -90,38 +88,67 @@ public Std(Class<?> raw, int kind) {
9088

9189
@Override
9290
public Object deserialize(JsonParser p, DeserializationContext ctxt)
93-
throws IOException
94-
{
95-
// For most types, use super impl; but GregorianCalendar also allows
96-
// integer value (timestamp), which needs separate handling
91+
throws IOException {
92+
// GregorianCalendar also allows integer value (timestamp),
93+
// which needs separate handling
9794
if (_kind == TYPE_G_CALENDAR) {
9895
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
9996
return _gregorianFromDate(ctxt, _parseDate(p, ctxt));
10097
}
10198
}
99+
// QName also allows object value, which needs separate handling
100+
if (_kind == TYPE_QNAME) {
101+
if (p.hasToken(JsonToken.START_OBJECT)) {
102+
return _parseQNameObject(p, ctxt);
103+
}
104+
}
102105
return super.deserialize(p, ctxt);
103106
}
104107

108+
private QName _parseQNameObject(JsonParser p, DeserializationContext ctxt) throws IOException {
109+
JsonNode tree = ctxt.readTree(p);
110+
111+
if (!tree.has("localPart")) {
112+
throw new JsonParseException("QName is missing required field: localPart");
113+
}
114+
115+
JsonNode localPart = tree.get("localPart");
116+
if (!localPart.isTextual()) {
117+
throw new JsonParseException("QName field \"localPart\" must be of type String.");
118+
}
119+
120+
if (tree.has("namespaceURI")) {
121+
JsonNode namespaceURI = tree.get("namespaceURI");
122+
123+
if (tree.has("prefix")) {
124+
JsonNode prefix = tree.get("prefix");
125+
return new QName(namespaceURI.asText(), localPart.asText(), prefix.asText());
126+
}
127+
128+
return new QName(namespaceURI.asText(), localPart.asText());
129+
} else {
130+
return new QName(localPart.asText());
131+
}
132+
}
133+
105134
@Override
106135
protected Object _deserialize(String value, DeserializationContext ctxt)
107-
throws IOException
108-
{
136+
throws IOException {
109137
switch (_kind) {
110-
case TYPE_DURATION:
111-
return _dataTypeFactory.newDuration(value);
112-
case TYPE_QNAME:
113-
return QName.valueOf(value);
114-
case TYPE_G_CALENDAR:
115-
Date d;
116-
try {
117-
d = _parseDate(value, ctxt);
118-
}
119-
catch (JsonMappingException e) {
120-
// try to parse from native XML Schema 1.0 lexical representation String,
121-
// which includes time-only formats not handled by parseXMLGregorianCalendarFromJacksonFormat(...)
122-
return _dataTypeFactory.newXMLGregorianCalendar(value);
123-
}
124-
return _gregorianFromDate(ctxt, d);
138+
case TYPE_DURATION:
139+
return _dataTypeFactory.newDuration(value);
140+
case TYPE_QNAME:
141+
return QName.valueOf(value);
142+
case TYPE_G_CALENDAR:
143+
Date d;
144+
try {
145+
d = _parseDate(value, ctxt);
146+
} catch (JsonMappingException e) {
147+
// try to parse from native XML Schema 1.0 lexical representation String,
148+
// which includes time-only formats not handled by parseXMLGregorianCalendarFromJacksonFormat(...)
149+
return _dataTypeFactory.newXMLGregorianCalendar(value);
150+
}
151+
return _gregorianFromDate(ctxt, d);
125152
}
126153
throw new IllegalStateException();
127154
}
@@ -135,8 +162,7 @@ protected Object _deserializeFromEmptyString(DeserializationContext ctxt) throws
135162
}
136163

137164
protected XMLGregorianCalendar _gregorianFromDate(DeserializationContext ctxt,
138-
Date d)
139-
{
165+
Date d) {
140166
if (d == null) {
141167
return null;
142168
}
@@ -148,5 +174,6 @@ protected XMLGregorianCalendar _gregorianFromDate(DeserializationContext ctxt,
148174
}
149175
return _dataTypeFactory.newXMLGregorianCalendar(calendar);
150176
}
177+
151178
}
152179
}

src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLSerializers.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
package com.fasterxml.jackson.databind.ext;
22

33
import java.io.IOException;
4+
import java.lang.reflect.Type;
45
import java.util.Calendar;
56

67
import javax.xml.datatype.Duration;
78
import javax.xml.datatype.XMLGregorianCalendar;
89
import javax.xml.namespace.QName;
910

11+
import com.fasterxml.jackson.annotation.JsonFormat;
1012
import com.fasterxml.jackson.core.*;
1113
import com.fasterxml.jackson.core.type.WritableTypeId;
1214
import com.fasterxml.jackson.databind.*;
1315
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
1416
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
17+
import com.fasterxml.jackson.databind.ser.BeanSerializer;
1518
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
19+
import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
1620
import com.fasterxml.jackson.databind.ser.Serializers;
17-
import com.fasterxml.jackson.databind.ser.std.CalendarSerializer;
18-
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
19-
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
21+
import com.fasterxml.jackson.databind.ser.std.*;
2022

2123
/**
2224
* Provider for serializers of XML types that are part of full JDK 1.5, but
@@ -34,9 +36,12 @@ public JsonSerializer<?> findSerializer(SerializationConfig config,
3436
JavaType type, BeanDescription beanDesc)
3537
{
3638
Class<?> raw = type.getRawClass();
37-
if (Duration.class.isAssignableFrom(raw) || QName.class.isAssignableFrom(raw)) {
39+
if (Duration.class.isAssignableFrom(raw)) {
3840
return ToStringSerializer.instance;
3941
}
42+
if (QName.class.isAssignableFrom(raw)) {
43+
return QNameSerializer.instance;
44+
}
4045
if (XMLGregorianCalendar.class.isAssignableFrom(raw)) {
4146
return XMLGregorianCalendarSerializer.instance;
4247
}
@@ -116,4 +121,52 @@ protected Calendar _convert(XMLGregorianCalendar input) {
116121
return (input == null) ? null : input.toGregorianCalendar();
117122
}
118123
}
124+
125+
public static class QNameSerializer
126+
extends StdSerializer<QName>
127+
implements ContextualSerializer
128+
{
129+
private static final long serialVersionUID = 1L;
130+
131+
public static JsonSerializer<?> instance = new QNameSerializer();
132+
133+
public QNameSerializer() {
134+
super(QName.class);
135+
}
136+
137+
@Override
138+
public JsonSerializer<?> createContextual(SerializerProvider serializers, BeanProperty property)
139+
throws JsonMappingException
140+
{
141+
JsonFormat.Value format = findFormatOverrides(serializers, property, handledType());
142+
if (format != null) {
143+
JsonFormat.Shape shape = format.getShape();
144+
if (shape == JsonFormat.Shape.OBJECT) {
145+
return this;
146+
}
147+
}
148+
return ToStringSerializer.instance;
149+
}
150+
151+
@Override
152+
public void serialize(QName value, JsonGenerator g, SerializerProvider provider) throws IOException {
153+
g.writeStartObject();
154+
g.writeObjectField("localPart", value.getLocalPart());
155+
if(!value.getNamespaceURI().isEmpty()) g.writeObjectField("namespaceURI", value.getNamespaceURI());
156+
if(!value.getPrefix().isEmpty()) g.writeObjectField("prefix", value.getPrefix());
157+
g.writeEndObject();
158+
}
159+
160+
@Override
161+
public final void serializeWithType(QName value, JsonGenerator g, SerializerProvider provider,
162+
TypeSerializer typeSer) throws IOException
163+
{
164+
g.writeObject(value);
165+
}
166+
167+
@Override
168+
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
169+
visitor.expectBooleanFormat(typeHint);
170+
}
171+
}
119172
}

src/test/java/com/fasterxml/jackson/databind/ext/MiscJavaXMLTypesReadWriteTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fasterxml.jackson.databind.ext;
22

3+
import com.fasterxml.jackson.annotation.JsonFormat;
34
import javax.xml.datatype.*;
45
import javax.xml.namespace.QName;
56
import org.junit.jupiter.api.Test;
@@ -40,6 +41,17 @@ public void testQNameSer() throws Exception
4041
assertEquals(q(qn.toString()), MAPPER.writeValueAsString(qn));
4142
}
4243

44+
@Test
45+
public void testQNameSerToObject() throws Exception {
46+
QName qn = new QName("http://abc", "tag", "prefix");
47+
48+
ObjectMapper mapper = jsonMapperBuilder()
49+
.withConfigOverride(QName.class, cfg -> cfg.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT)))
50+
.build();
51+
52+
assertEquals(a2q("{'localPart':'tag','namespaceURI':'http://abc','prefix':'prefix'}"), mapper.writeValueAsString(qn));
53+
}
54+
4355
@Test
4456
public void testDurationSer() throws Exception
4557
{
@@ -121,6 +133,21 @@ public void testQNameDeser() throws Exception
121133
assertEquals("", qn.getLocalPart());
122134
}
123135

136+
@Test
137+
public void testQNameDeserFromObject() throws Exception
138+
{
139+
String qstr = a2q("{'namespaceURI':'http://abc','localPart':'tag','prefix':'prefix'}");
140+
ObjectMapper mapper = jsonMapperBuilder()
141+
.withConfigOverride(QName.class, cfg -> cfg.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT)))
142+
.build();
143+
144+
QName qn = mapper.readValue(qstr, QName.class);
145+
146+
assertEquals("http://abc", qn.getNamespaceURI());
147+
assertEquals("tag", qn.getLocalPart());
148+
assertEquals("prefix", qn.getPrefix());
149+
}
150+
124151
@Test
125152
public void testXMLGregorianCalendarDeser() throws Exception
126153
{
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.fasterxml.jackson.databind.ext;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import com.fasterxml.jackson.core.JsonProcessingException;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.Arguments;
9+
import org.junit.jupiter.params.provider.MethodSource;
10+
11+
import javax.xml.namespace.QName;
12+
import java.util.stream.Stream;
13+
14+
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
16+
class QNameAsObjectReadWrite4771Test extends DatabindTestUtil
17+
{
18+
19+
private final ObjectMapper MAPPER = newJsonMapper();
20+
21+
static class BeanWithQName {
22+
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
23+
public QName qname;
24+
25+
public BeanWithQName() {
26+
}
27+
28+
public BeanWithQName(QName qName) {
29+
this.qname = qName;
30+
}
31+
}
32+
33+
34+
@ParameterizedTest
35+
@MethodSource("provideAllPerumtationsOfQNameConstructor")
36+
void testQNameWithObjectSerialization(QName originalQName) throws JsonProcessingException {
37+
BeanWithQName bean = new BeanWithQName(originalQName);
38+
39+
String json = MAPPER.writeValueAsString(bean);
40+
41+
QName deserializedQName = MAPPER.readValue(json, BeanWithQName.class).qname;
42+
43+
assertEquals(originalQName.getLocalPart(), deserializedQName.getLocalPart());
44+
assertEquals(originalQName.getNamespaceURI(), deserializedQName.getNamespaceURI());
45+
assertEquals(originalQName.getPrefix(), deserializedQName.getPrefix());
46+
}
47+
48+
static Stream<Arguments> provideAllPerumtationsOfQNameConstructor() {
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+
56+
}

0 commit comments

Comments
 (0)