Skip to content
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

Jackson Upgrade 2.11.4 to 2.16.1: Base class field serialization/deserialization not working properly #4354

Closed
1 task done
arpithnayak opened this issue Jan 31, 2024 · 4 comments

Comments

@arpithnayak
Copy link

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

I upgraded jackson library in my codebase from 2.11.4 to 2.16.1 and the below test case that was working fine before is now failing. In one run I retained the JsonTypeInfo annotation and I get the following exception:

java.lang.IllegalStateException: com.fasterxml.jackson.databind.JsonMappingException: Can not write a field name, expecting a value (through reference chain: SendEmailAction["type"])

If I remove the @JsonTypeInfo annotation, the assert statement fails complaining that the deserilized MyAction object is not an instance of SendEmailAction class.

Any idea why this change in jackson behaviour?

Version Information

2.16.1 on Macbook Intel Ventura.

Reproduction

Code attached for reference:

This is the existing base class.

/**
  * Defines an action.
  */
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM,
         include = JsonTypeInfo.As.EXISTING_PROPERTY,
         property = "type",
         visible = true)
 @JsonTypeIdResolver(MyActionIdResolver.class)
 public class MyAction implements Serializable {

     private static final long serialVersionUID = 1L;

     @NotNull
     @JsonProperty("type")
     private MyType type;

     MyAction() {
    }
    
    public MyAction(final MyType type) {
        this.type = type;
    }
    public MyType getType() {
        return this.type;
    }
    public void setType(final MyType type) {
        this.type = type;
    }
    public boolean isCompliant() {
        return false;
    }
    public boolean isTemplate() {
        return false;
    }
    @Override
    public boolean equals(final Object other) {
        if (other == null) {
            return false;
        }
        if (getClass() != other.getClass()) {
            return false;
        }
        final MyAction rhs = (MyAction)other;
        return new EqualsBuilder()
                .append(this.type, rhs.type)
                .isEquals();
    }
    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(this.type)
                .toHashCode();
    }
}

And this is the derived class that extends the MyAction class.

/**
 * Defines send email action.
 */
public class SendEmailAction extends MyAction {

    private static final long serialVersionUID = 1L;

    @NotBlank
    @JsonProperty("subject")
    private String subject;

    @JsonProperty("compliant")
    private boolean compliant;

    @JsonProperty("template")
    private boolean template;

    SendEmailAction() {
        super(MyType.SEND_EMAIL);
    }

    public SendEmailAction(final String subject, final boolean compliant, final boolean template) {
        this();
        this.subject = subject;
        this.compliant = compliant;
        this.template = template;
    }

    public String getSubject() {
        return this.subject;
    }

    public void setSubject(final String subject) {
        this.subject = subject;
    }

    @Override
    public boolean isCompliant() {
        return this.compliant;
    }

    public void setCompliant(final boolean compliant) {
        this.compliant = compliant;
    }

    @Override
    public boolean isTemplate() {
        return this.template;
    }

    public void setTemplate(final boolean template) {
        this.template = template;
    }

    @Override
    public boolean equals(final Object other) {
        if (other == null) {
            return false;
        }
        if (getClass() != other.getClass()) {
            return false;
        }
        final SendEmailAction rhs = (SendEmailAction)other;
        return new EqualsBuilder()
                .appendSuper(super.equals(other))
                .append(this.subject, rhs.subject)
                .append(this.compliant, rhs.compliant)
                .append(this.template, rhs.template)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .appendSuper(super.hashCode())
                .append(this.emailSubject)
                .append(this.emailBody)
                .append(this.compliant)
                .append(this.useTemplate)
                .toHashCode();
    }
}

This is the ENUM class.

public enum MyType {
    SEND_EMAIL,
    DO_NOTHING
}

This is the resolver class.

public class MyActionIdResolver extends TypeIdResolverBase {

    private JavaType superType;

    public MyActionIdResolver() {

    }

    @Override
    public void init(final JavaType baseType) {
        this.superType = baseType;
    }

    @Override
    public JavaType typeFromId(final DatabindContext context, final String id) {
        Class<?> subType = null;
        switch (MyType.valueOf(id)) {
        case SEND_EMAIL:
            subType = SendEmailAction.class;
            break;
        case DO_NOTHING:
        default:
            subType = MyAction.class;
        }

        return context.constructSpecializedType(this.superType, subType);
    }

    @Override
    public Id getMechanism() {
        return Id.CUSTOM;
    }

    @Override
    public String idFromValue(final Object value) {
       return null;
    }

    @Override
    public String idFromValueAndType(final Object value, final Class<?> suggestedType) {
        return null;
    }

}

And the test class that fails post the upgrade.

    public void test() throws JsonProcessingException {

            final StringWriter w = new StringWriter();

        final JsonMapper m = new JsonMapper()
                .configure(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS, false)
                .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        m.getMapper().configOverride(Map.class).setInclude(JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_NULL));
        m.getMapper().writeValue(w, MyAction.class); // <- failing here if JsonTypeInfo annotation is used
        String toJson = w.toString();      

        MyAction fromJson = m.getMapper().writeValue(toJson, MyAction.class);

        assertThat(fromJson, notNullValue());
        assertThat(fromJson, is(instanceOf(SendEmailAction.class))); // <- fails here due to type erasure
    }

Expected behavior

There should be no problem in serializing just as before.

Object deserilized using MyAction.class is and should be an instance of SendEmailAction.class.

Additional context

No response

@arpithnayak arpithnayak added the to-evaluate Issue that has been received but not yet evaluated label Jan 31, 2024
@JooHyukKim
Copy link
Member

JooHyukKim commented Jan 31, 2024

Any idea why this change in jackson behaviour?

Though I am not sure whether the behavior change (if true) is intended or not, implementing

  • String idFromValue(final Object value)
  • String idFromValueAndType(final Object value, final Class<?> suggestedType)

... methods instead of returning null might solve the error if that's what you are looking for.

@cowtowncoder
Copy link
Member

What would help here would be a minimal reproduction. Example as-is is unfortunately big too big as regression test.
I don't think there has been anything specific to try to break this usage, but it is likely some fix for another problem caused this change.

And yes, idFromValue() should probably not return null; I am not 100% sure if that should be supported to mean "do not add Type Id" or simply fail.

@arpithnayak
Copy link
Author

Adding code in the idFromValue() method fixed the issue. Although why it worked previously with null (in 2.11.4) and stopped working in the current version is not clear. There didn't seem to be any documentation around this for me to read up on. Anyways, thank you for suggesting the fix.

@cowtowncoder
Copy link
Member

As I said, we do not really know why it was seemingly working -- I don't think support was necessarily intentional, nor breakage. Would be good to find if this could be improved, but we still do not have small enough reproduction to really achieve that.

But it is good you have a workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants