Skip to content

Commit

Permalink
Fix #4771: Support OBJECT shape for QNAME serialization and deseriali…
Browse files Browse the repository at this point in the history
…zation. (#4968)
  • Loading branch information
mcvayc authored Feb 14, 2025
1 parent dd929e2 commit 70511f0
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 5 deletions.
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"))
);
}
}

0 comments on commit 70511f0

Please sign in to comment.