question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

`XmlMapper` 2.12 regression: no default no-arg ctor found

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:18 (13 by maintainers)

github_iconTop GitHub Comments

1reaction
cowtowncodercommented, Sep 4, 2021

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:

  1. Change interpretation of <root></root> (empty element) to produce START_OBJECT / END_OBJECT pair (instead of VALUE_STRING). This would likely cause other regression, but is a possibility.
  2. Change StdDeserializer handling of “empty value” to allow use of Properties-based Creator, passing “all absent” values. Actually code goes to BeanDeserializerBase.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.

0reactions
henrik242commented, Sep 27, 2022
Read more comments on GitHub >

github_iconTop Results From Across the Web

Jackson XML works with class but not with record
Adding a default no-arguments constructor makes no difference, but changing from record to class (adding all the necessary stuff) doew. What am ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found