diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java index 595df82fa5..908073483c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java @@ -430,6 +430,19 @@ public enum DeserializationFeature implements ConfigFeature */ READ_ENUMS_USING_TO_STRING(false), + /** + * Feature that determines standard deserialization mechanism used for + * QName values: if enabled, QNames are assumed to have been serialized using + * return value of QName.toString(); + * if disabled, it is assumed that the QName was serialized as an object. + *

+ * Note: this feature should usually have same value + * as {@link SerializationFeature#WRITE_QNAMES_USING_TO_STRING}. + *

+ * Feature is disabled by default. + */ + READ_QNAMES_USING_VALUE_OF(true), + /** * Feature that allows unknown Enum values to be parsed as {@code null} values. * If disabled, unknown Enum values will throw exceptions. diff --git a/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java index a16e5cbeff..013893a8ba 100644 --- a/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/SerializationFeature.java @@ -292,6 +292,18 @@ public enum SerializationFeature implements ConfigFeature */ WRITE_ENUMS_USING_TO_STRING(false), + /** + * Feature that determines standard serialization mechanism used for + * QName values: if enabled, return value of QName.toString() + * is used; if disabled, the QName is serialized as an object. + *

+ * Note: this feature should usually have same value + * as {@link DeserializationFeature#READ_QNAMES_USING_VALUE_OF}. + *

+ * Feature is disabled by default. + */ + WRITE_QNAMES_USING_TO_STRING(true), + /** * Feature that determines whether Java Enum values are serialized * as numbers (true), or textual values (false). If textual values are diff --git a/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLDeserializers.java index 76609a3914..d42d7504c2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLDeserializers.java @@ -7,6 +7,8 @@ import javax.xml.namespace.QName; import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.type.TypeReference; + import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.Deserializers; import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; @@ -40,7 +42,10 @@ public JsonDeserializer findBeanDeserializer(JavaType type, { Class raw = type.getRawClass(); if (raw == QName.class) { - return new Std(raw, TYPE_QNAME); + if (config == null || config.isEnabled(DeserializationFeature.READ_QNAMES_USING_VALUE_OF)) { + return new Std(raw, TYPE_QNAME); + } + return new QNameObjectDeserializer(); } if (raw == XMLGregorianCalendar.class) { return new Std(raw, TYPE_G_CALENDAR); @@ -149,4 +154,27 @@ protected XMLGregorianCalendar _gregorianFromDate(DeserializationContext ctxt, return _dataTypeFactory.newXMLGregorianCalendar(calendar); } } + + private class QNameObjectDeserializer extends JsonDeserializer { + private static final long serialVersionUID = 1L; + public static final TypeReference> STRING_MAP_TYPE_REFERENCE = new TypeReference<>() {}; + + @Override + public QName deserialize(final JsonParser p, final DeserializationContext ctxt) + throws IOException + { + Map map; + try { + map = p.readValueAs(STRING_MAP_TYPE_REFERENCE); + } catch (IOException e) { + throw new JsonMappingException(p, "Unable to parse the QName as an object.", e); + } + + return new QName( + map.getOrDefault("namespaceURI", ""), + map.getOrDefault("localPart", ""), + map.getOrDefault("prefix", "") + ); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLSerializers.java b/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLSerializers.java index 219d9a43db..aeeeb7968f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLSerializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ext/CoreXMLSerializers.java @@ -34,9 +34,15 @@ public JsonSerializer findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { Class raw = type.getRawClass(); - if (Duration.class.isAssignableFrom(raw) || QName.class.isAssignableFrom(raw)) { + if (Duration.class.isAssignableFrom(raw)){ return ToStringSerializer.instance; } + if (QName.class.isAssignableFrom(raw)) { + if (config.isEnabled(SerializationFeature.WRITE_QNAMES_USING_TO_STRING)) { + return ToStringSerializer.instance; + } + return null; + } if (XMLGregorianCalendar.class.isAssignableFrom(raw)) { return XMLGregorianCalendarSerializer.instance; } diff --git a/src/test/java/com/fasterxml/jackson/databind/ext/QNameAsObjectReadWrite4771Test.java b/src/test/java/com/fasterxml/jackson/databind/ext/QNameAsObjectReadWrite4771Test.java new file mode 100644 index 0000000000..52648fd734 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ext/QNameAsObjectReadWrite4771Test.java @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.databind.ext; + +import java.util.stream.Stream; +import javax.xml.namespace.QName; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.newJsonMapper; + +class QNameAsObjectReadWrite4771Test { + + private final ObjectMapper MAPPER = newJsonMapper(); + + @ParameterizedTest + @MethodSource("provideAllPerumtationsOfQNameConstructor") + void testQNameWithObjectSerialization(QName originalQName) throws JsonProcessingException { + String json = MAPPER + .disable(SerializationFeature.WRITE_QNAMES_USING_TO_STRING) + .writeValueAsString(originalQName); + + QName deserializedQName = MAPPER + .disable(DeserializationFeature.READ_QNAMES_USING_VALUE_OF) + .readValue(json, QName.class); + + assertEquals(originalQName, deserializedQName); + } + + static Stream provideAllPerumtationsOfQNameConstructor() { + return Stream.of( + Arguments.of(new QName("test-local-part")), + Arguments.of(new QName("test-namespace-uri", "test-local-part")), + Arguments.of(new QName("test-namespace-uri", "test-local-part", "test-prefix")) + ); + } + +} \ No newline at end of file