Skip to content

Fix #4771: Support OBJECT shape for QNAME serialization and deserialization. #4968

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Project: jackson-databind
Map object is ignored when Map key type not defined
(reported by @devdanylo)
(fix by Joo-Hyuk K)
#4771: `QName` (de)serialization ignores prefix
(reported by @jpraet)
(fix contributed by @mcvayc)
#4772: Serialization and deserialization issue of sub-types used with
`JsonTypeInfo.Id.DEDUCTION` where sub-types are Object and Array
(reported by Eduard G)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,50 @@ public Std(Class<?> raw, int kind) {
public Object deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException
{
// For most types, use super impl; but GregorianCalendar also allows
// integer value (timestamp), which needs separate handling
// GregorianCalendar also allows integer value (timestamp),
// which needs separate handling
if (_kind == TYPE_G_CALENDAR) {
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
return _gregorianFromDate(ctxt, _parseDate(p, ctxt));
}
}
// QName also allows object value, which needs separate handling
if (_kind == TYPE_QNAME) {
if (p.hasToken(JsonToken.START_OBJECT)) {
return _parseQNameObject(p, ctxt);
}
}
return super.deserialize(p, ctxt);
}

private QName _parseQNameObject(JsonParser p, DeserializationContext ctxt)
throws IOException
{
JsonNode tree = ctxt.readTree(p);

JsonNode localPart = tree.get("localPart");
if (localPart == null) {
ctxt.reportInputMismatch(this,
"Object value for `QName` is missing required property 'localPart'");
}

if (!localPart.isTextual()) {
ctxt.reportInputMismatch(this,
"Object value property 'localPart' for `QName` must be of type STRING, not %s",
localPart.getNodeType());
}

JsonNode namespaceURI = tree.get("namespaceURI");
if (namespaceURI != null) {
if (tree.has("prefix")) {
JsonNode prefix = tree.get("prefix");
return new QName(namespaceURI.asText(), localPart.asText(), prefix.asText());
}
return new QName(namespaceURI.asText(), localPart.asText());
}
return new QName(localPart.asText());
}

@Override
protected Object _deserialize(String value, DeserializationContext ctxt)
throws IOException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.*;
Expand Down Expand Up @@ -34,9 +35,12 @@ 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)) {
return QNameSerializer.instance;
}
if (XMLGregorianCalendar.class.isAssignableFrom(raw)) {
return XMLGregorianCalendarSerializer.instance;
}
Expand Down Expand Up @@ -116,4 +120,73 @@ protected Calendar _convert(XMLGregorianCalendar input) {
return (input == null) ? null : input.toGregorianCalendar();
}
}

/**
* @since 2.19
*/
public static class QNameSerializer
extends StdSerializer<QName>
implements ContextualSerializer
{
private static final long serialVersionUID = 1L;

public final static JsonSerializer<?> instance = new QNameSerializer();

public QNameSerializer() {
super(QName.class);
}

@Override
public JsonSerializer<?> createContextual(SerializerProvider serializers, BeanProperty property)
throws JsonMappingException
{
JsonFormat.Value format = findFormatOverrides(serializers, property, handledType());
if (format != null) {
JsonFormat.Shape shape = format.getShape();
if (shape == JsonFormat.Shape.OBJECT) {
return this;
}
}
return ToStringSerializer.instance;
}

@Override
public void serialize(QName value, JsonGenerator g, SerializerProvider ctxt)
throws IOException
{
g.writeStartObject(value);
serializeProperties(value, g, ctxt);
g.writeEndObject();
}

@Override
public final void serializeWithType(QName value, JsonGenerator g, SerializerProvider ctxt,
TypeSerializer typeSer)
throws IOException
{
WritableTypeId typeIdDef = typeSer.writeTypePrefix(g,
typeSer.typeId(value, JsonToken.START_OBJECT));
serializeProperties(value, g, ctxt);
typeSer.writeTypeSuffix(g, typeIdDef);
}

private void serializeProperties(QName value, JsonGenerator g, SerializerProvider ctxt)
throws IOException
{
g.writeStringField("localPart", value.getLocalPart());
if (!value.getNamespaceURI().isEmpty()) {
g.writeStringField("namespaceURI", value.getNamespaceURI());
}
if (!value.getPrefix().isEmpty()) {
g.writeStringField("prefix", value.getPrefix());
}
}

@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
throws JsonMappingException {
/*JsonObjectFormatVisitor v =*/ visitor.expectObjectFormat(typeHint);
// TODO: would need to visit properties too, see `BeanSerializerBase`
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import javax.xml.namespace.QName;
import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import com.fasterxml.jackson.databind.testutil.NoCheckSubTypeValidator;
import com.fasterxml.jackson.databind.type.TypeFactory;
Expand Down Expand Up @@ -34,12 +36,24 @@ public class MiscJavaXMLTypesReadWriteTest
*/

@Test
public void testQNameSer() throws Exception
public void testQNameSerDefault() throws Exception
{
QName qn = new QName("http://abc", "tag", "prefix");
assertEquals(q(qn.toString()), MAPPER.writeValueAsString(qn));
}

@Test
public void testQNameSerToObject() throws Exception
{
QName qn = new QName("http://abc", "tag", "prefix");

ObjectMapper mapper = jsonMapperBuilder()
.withConfigOverride(QName.class, cfg -> cfg.setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT)))
.build();

assertEquals(a2q("{'localPart':'tag','namespaceURI':'http://abc','prefix':'prefix'}"), mapper.writeValueAsString(qn));
}

@Test
public void testDurationSer() throws Exception
{
Expand Down Expand Up @@ -121,6 +135,37 @@ public void testQNameDeser() throws Exception
assertEquals("", qn.getLocalPart());
}

@Test
public void testQNameDeserFromObject() throws Exception
{
String qstr = a2q("{'namespaceURI':'http://abc','localPart':'tag','prefix':'prefix'}");
// Ok to read with standard ObjectMapper, no `@JsonFormat` needed
QName qn = MAPPER.readValue(qstr, QName.class);

assertEquals("http://abc", qn.getNamespaceURI());
assertEquals("tag", qn.getLocalPart());
assertEquals("prefix", qn.getPrefix());
}

@Test
public void testQNameDeserFail() throws Exception
{
try {
MAPPER.readValue("{}", QName.class);
fail("Should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Object value for `QName` is missing required property 'localPart'");
}

try {
MAPPER.readValue(a2q("{'localPart': 123}"), QName.class);
fail("Should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Object value property 'localPart'");
verifyException(e, "must be of type STRING, not NUMBER");
}
}

@Test
public void testXMLGregorianCalendarDeser() throws Exception
{
Expand Down Expand Up @@ -149,7 +194,6 @@ public void testDurationDeser() throws Exception
/**********************************************************************
*/


@Test
public void testPolymorphicXMLGregorianCalendar() throws Exception
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.annotation.JsonFormat;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;

class QNameAsObjectReadWrite4771Test extends DatabindTestUtil
{
private final ObjectMapper MAPPER = newJsonMapper();

static class BeanWithQName {
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public QName qname;

BeanWithQName() { }

public BeanWithQName(QName qName) {
this.qname = qName;
}
}

@ParameterizedTest
@MethodSource("provideAllPerumtationsOfQNameConstructor")
void testQNameWithObjectSerialization(QName originalQName) throws JsonProcessingException
{
BeanWithQName bean = new BeanWithQName(originalQName);

String json = MAPPER.writeValueAsString(bean);

QName deserializedQName = MAPPER.readValue(json, BeanWithQName.class).qname;

assertEquals(originalQName.getLocalPart(), deserializedQName.getLocalPart());
assertEquals(originalQName.getNamespaceURI(), deserializedQName.getNamespaceURI());
assertEquals(originalQName.getPrefix(), deserializedQName.getPrefix());
}

static Stream<Arguments> 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"))
);
}
}