jackson-dataformat-xml: `XmlMapper` 2.12 regression: no default no-arg ctor found
As described first in FasterXML/jackson-module-kotlin#396, 2.12.0 has introduced a regression to the XmlMapper
. I have managed to reproduce the regression in the below Java snippet, which works on 2.11.4, but fails on 2.12.{0…4}:
package com.vlkan.jackson;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MixinTest {
interface Problem {
String DEFAULT_TYPE = "about:blank";
int DEFAULT_STATUS = 500;
String getType();
int getStatus();
}
static class DefaultProblem implements Problem {
private final String type;
private final int status;
/**
* This is required to workaround Jackson's missing support for static
* {@link JsonCreator}s in mix-ins. That is, we need to define the
* creator on a constructor in the mix-in that is matching with a
* constructor here too.
*
* @see <a href="https://github.com/FasterXML/jackson-databind/issues/1820">jackson-databind issue 1820</a>
*/
DefaultProblem(String type, Integer status) {
this.type = type != null ? type : Problem.DEFAULT_TYPE;
this.status = status != null ? status : Problem.DEFAULT_STATUS;
}
@Override
public String getType() {
return type;
}
@Override
public int getStatus() {
return status;
}
}
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "type",
defaultImpl = DefaultProblem.class,
visible = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonRootName("problem")
interface ProblemMixIn extends Problem {
@Override
@JsonProperty("type")
String getType();
@Override
@JsonProperty("status")
int getStatus();
}
abstract static class DefaultProblemMixIn extends DefaultProblem {
@JsonCreator
DefaultProblemMixIn(
@JsonProperty("type") String type,
@JsonProperty("status") Integer status) {
super(type, status);
throw new IllegalStateException(
"mix-in constructor is there only for extracting the JSON mapping, " +
"it should not have been called");
}
}
static class ProblemModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
registerMixIns(context);
}
private static void registerMixIns(SetupContext context) {
context.setMixInAnnotations(DefaultProblem.class, DefaultProblemMixIn.class);
context.setMixInAnnotations(Problem.class, ProblemMixIn.class);
}
}
private static final ProblemModule MODULE = new ProblemModule();
private static final ObjectMapper JSON_MAPPER = new ObjectMapper().registerModule(MODULE);
private static final XmlMapper XML_MAPPER = (XmlMapper) new XmlMapper().registerModule(MODULE);
@Test
void test_empty_Problem_JSON_deserialization() throws IOException {
byte[] problemJsonBytes = "{}".getBytes(StandardCharsets.UTF_8);
Problem problem = JSON_MAPPER.readValue(problemJsonBytes, Problem.class);
assertEquals(Problem.DEFAULT_TYPE, problem.getType());
assertEquals(Problem.DEFAULT_STATUS, problem.getStatus());
}
@Test
void test_empty_Problem_XML_deserialization() throws IOException {
byte[] problemXmlBytes = "<problem/>".getBytes(StandardCharsets.UTF_8);
Problem problem = XML_MAPPER.readValue(problemXmlBytes, Problem.class);
assertEquals(Problem.DEFAULT_TYPE, problem.getType());
assertEquals(Problem.DEFAULT_STATUS, problem.getStatus());
}
}
Both tests pass on 2.11.4, whereas, on 2.12.{0…4}, test_empty_Problem_JSON_deserialization()
passes and
test_empty_Problem_XML_deserialization()
fails with the following message:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.vlkan.jackson.MixinTest$DefaultProblem` (although at least one Creator exists): no default no-arguments constructor found
at [Source: (byte[])"<problem/>"; line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1588)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1213)
at com.fasterxml.jackson.databind.deser.ValueInstantiator.createUsingDefault(ValueInstantiator.java:248)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:275)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.getEmptyValue(BeanDeserializerBase.java:1042)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromEmptyString(StdDeserializer.java:322)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromString(StdDeserializer.java:270)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1495)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:207)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:197)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedUsingDefaultImpl(AsPropertyTypeDeserializer.java:194)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:96)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
at com.fasterxml.jackson.dataformat.xml.deser.XmlDeserializationContext.readRootValue(XmlDeserializationContext.java:91)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3609)
at com.vlkan.jackson.MixinTest.test_empty_Problem_XML_deserialization(MixinTest.java:129)
...
In order to make test_empty_Problem_XML_deserialization()
pass on 2.12.{0…4}, one needs to add a no-arg ctor to DefaultProblem
.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 18 (13 by maintainers)
Commits related to this issue
- Adds test to reproduce the `no default no-arg ctor` regression in #491. — committed to vy/jackson-dataformat-xml by vy 3 years ago
- Adds test to reproduce the `no default no-arg ctor` regression in #491. (#492) Co-authored-by: Volkan Yazıcı <volkan@yazi.ci> — committed to FasterXML/jackson-dataformat-xml by vy 3 years ago
- Simplify #491 test a bit, to remove mix-ins — committed to FasterXML/jackson-dataformat-xml by cowtowncoder 3 years ago
- Move formerly failing tests (wrt #491, #538) as non-failing — committed to FasterXML/jackson-dataformat-xml by cowtowncoder 2 years ago
- Add test that fails in 2.14.x See also https://github.com/FasterXML/jackson-dataformat-xml/issues/491 — committed to henrik242/jackson-xml-problem by henrik242 2 years ago
- Move #491 test to failing for 3.0 (not 100% sure why fails but need to unblock CI) — committed to FasterXML/jackson-dataformat-xml by cowtowncoder 2 years ago
Ok. I added some notes on the failing test. This is not an easy problem to solve; basically there seems to be 2 ways to work around it. Either:
<root></root>
(empty element) to produceSTART_OBJECT
/END_OBJECT
pair (instead ofVALUE_STRING
). This would likely cause other regression, but is a possibility.StdDeserializer
handling of “empty value” to allow use of Properties-based Creator, passing “all absent” values. Actually code goes toBeanDeserializerBase.getEmptyValue()
, which is where this would need to be done.Of these (2) would work better and would probably be the way to go. I don’t have time to pursue this quite now, but wanted to add a note if anyone else might want to try it.
Other than this, adding the no-arguments constructor also works around the issue.