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

Deserialization with Builder, External type id, @JsonCreator failing #4742

Open
1 task done
bernd opened this issue Oct 9, 2024 · 5 comments
Open
1 task done

Deserialization with Builder, External type id, @JsonCreator failing #4742

bernd opened this issue Oct 9, 2024 · 5 comments
Labels
2.18 has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue

Comments

@bernd
Copy link

bernd commented Oct 9, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Starting with version 2.18, we get the following exception when deserializing our auto-value-based objects.

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Deserialization (of [simple type, class com.github.bernd.javatests.jackson.JacksonBuilderCreatorSubtype$Animal]) with Builder, External type id, @JsonCreator not yet implemented
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 3, column: 6] (through reference chain: com.github.bernd.javatests.jackson.JacksonBuilderCreatorSubtype$Animals["animals"]->java.util.ArrayList[0])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1888)
	at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.deserializeUsingPropertyBasedWithExternalTypeId(BuilderBasedDeserializer.java:828)
	at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.deserializeWithExternalTypeId(BuilderBasedDeserializer.java:764)
	at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.deserializeFromObject(BuilderBasedDeserializer.java:318)
	at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.deserialize(BuilderBasedDeserializer.java:220)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:361)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:246)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:30)
	at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:310)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4917)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3828)
	at com.github.bernd.javatests.jackson.JacksonBuilderCreatorSubtype.main(JacksonBuilderCreatorSubtype.java:101)

Version Information

2.18.0

Reproduction

This example triggers the bug. Our code uses Google's auto-value, but I wrote the example without auto-value to make it easier to debug. The code works with 2.17 but fails with 2.18.

package com.github.bernd.javatests.jackson;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;

import java.util.List;

public class JacksonBuilderCreatorSubtype {
    public static class Animals {
        @JsonProperty("animals")
        public List<Animal> animals;
    }

    @JsonDeserialize(builder = Animal.Builder.class)
    public static class Animal {
        @JsonProperty("kind")
        public String kind;

        @JsonProperty("properties")
        public AnimalProperties properties;

        @Override
        public String toString() {
            return "Animal{kind='" + kind + '\'' + ", properties=" + properties + '}';
        }

        public static abstract class Builder {
            @JsonProperty("kind")
            public abstract Builder kind(String kind);

            @JsonProperty("properties")
            @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "kind")
            @JsonSubTypes({
                    @JsonSubTypes.Type(name = "bird", value = BirdProperties.class),
                    @JsonSubTypes.Type(name = "mammal", value = MammalProperties.class)
            })
            public abstract Builder properties(AnimalProperties properties);

            @JsonCreator
            public static BuilderImpl create() {
                return new BuilderImpl();
            }

            public abstract Animal build();
        }

        public static class BuilderImpl extends Builder {
            private String kind;
            private AnimalProperties properties;

            public BuilderImpl kind(String kind) {
                this.kind = kind;
                return this;
            }

            public BuilderImpl properties(AnimalProperties properties) {
                this.properties = properties;
                return this;
            }

            @Override
            public Animal build() {
                final var animal = new Animal();
                animal.kind = kind;
                animal.properties = properties;
                return animal;
            }
        }
    }

    public interface AnimalProperties {
    }

    public static class MammalProperties implements AnimalProperties {
        @JsonProperty("num_teeth")
        public int teeth;

        @Override
        public String toString() {
            return "MammalProperties{teeth=" + teeth + '}';
        }
    }

    public static class BirdProperties implements AnimalProperties {
        @JsonProperty("color")
        public String color;

        @Override
        public String toString() {
            return "BirdProperties{color='" + color + '\'' + '}';
        }
    }

    public static void main(String[] args) throws Exception {
        final var jsonMapper = JsonMapper.builder().build();

        final var animals = jsonMapper.readValue("""
                {
                  "animals": [
                    {"kind": "bird", "properties": {"color": "yellow"}},
                    {"kind": "mammal", "properties": {"num_teeth": 2}}
                  ]
                }
                """, Animals.class);

        animals.animals.forEach(System.out::println);
    }
}

Expected behavior

No response

Additional context

No response

@bernd bernd added the to-evaluate Issue that has been received but not yet evaluated label Oct 9, 2024
@cowtowncoder cowtowncoder added has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue 2.18 labels Oct 10, 2024
@JooHyukKim
Copy link
Member

JooHyukKim commented Oct 20, 2024

Apologies for late response 🙏🏼 First of all, thank you for reporting this!
Seems like this is regression.
I started looking into this.

@JooHyukKim
Copy link
Member

Seems like code path changed by #4515.

So before 2.18 in BuilderBasedDeserializer, used to call deserializeWithExternalTypeId.
But starting 2.18 version is now calling deserializeUsingPropertyBasedWithExternalTypeId.

    protected Object deserializeWithExternalTypeId(JsonParser p, DeserializationContext ctxt)
        throws IOException
    {
        if (_propertyBasedCreator != null) {
            return deserializeUsingPropertyBasedWithExternalTypeId(p, ctxt);
        }
        return deserializeWithExternalTypeId(p, ctxt, _valueInstantiator.createUsingDefault(ctxt));
    }

The _propertyBasedCreator is just empty one.

@JooHyukKim
Copy link
Member

Wrote #4757 to start talking with @cowtowncoder possible direction to fix.

cowtowncoder pushed a commit that referenced this issue Oct 24, 2024
@cowtowncoder cowtowncoder removed the to-evaluate Issue that has been received but not yet evaluated label Nov 13, 2024
@cowtowncoder cowtowncoder changed the title Deserialization with Builder, External type id, @JsonCreator not yet implemented Deserialization with Builder, External type id, @JsonCreator failing Nov 13, 2024
@cowtowncoder
Copy link
Member

Surprisingly, will be fixed by #4799 along with #4777.

@bernd
Copy link
Author

bernd commented Nov 13, 2024

Thank you! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.18 has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue
Projects
None yet
Development

No branches or pull requests

3 participants