JsonTypeInfo.As.EXTERNAL_PROPERTY does not work with record wrappers · Issue #3342 · FasterXML/jackson-databind (original) (raw)

Describe the bug
When I try to use JsonTypeInfo.As.EXTERNAL_PROPERTY inside a record, I get

com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `my.company.fastcheck.analyzer.JacksonExternalTypeIdTest$Parent`, problem: Internal error: no creator index for property 'child' (of type com.fasterxml.jackson.databind.deser.impl.FieldProperty)

Note that it works with normal classes. Code examples below.

Version information
2.13.0

To Reproduce

Using a record as wrapping object: (Fails)

import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.DatabindContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;

import java.io.IOException;

class JacksonExternalTypeIdTest {

@Test
void testExternalTypeIdPropertyInsideRecord() throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    Parent parent = objectMapper.readValue("""
        {"type": "CHILLED", "child": {}}
    """, Parent.class);
    Assertions.assertTrue(parent.child instanceof ChilledChild);
}

public enum ParentType {
    CHILLED,
    AGGRESSIVE
}

public static record Parent(
        ParentType type,
        @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
        @JsonTypeIdResolver(ChildBaseByParentTypeResolver.class)
        ChildBase child
) {}

public static interface ChildBase {
}

public static record AggressiveChild(String someString) implements ChildBase {
}

public static record ChilledChild(String someString) implements ChildBase {
}

public static class ChildBaseByParentTypeResolver extends TypeIdResolverBase {

    private JavaType superType;

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

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

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        Class<?> subType = switch (id) {
            case "CHILLED" -> ChilledChild.class;
            case "AGGRESSIVE" -> AggressiveChild.class;
            default -> throw new IllegalArgumentException();
        };
        return context.constructSpecializedType(superType, subType);
    }

    @Override
    public String idFromValue(Object value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String idFromValueAndType(Object value, Class<?> suggestedType) {
        throw new UnsupportedOperationException();
    }
}

}

Using a class as wrapping object: (Passes)

import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.DatabindContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;

import java.io.IOException;

class JacksonExternalTypeIdTest {

@Test
void testExternalTypeIdPropertyInsideRecord() throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    Parent parent = objectMapper.readValue("""
                {"type": "CHILLED", "child": {}}
            """, Parent.class);
    Assertions.assertTrue(parent.child instanceof ChilledChild);
}

public enum ParentType {
    CHILLED,
    AGGRESSIVE
}

public static final class Parent {
    private final ParentType type;

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
    @JsonTypeIdResolver(ChildBaseByParentTypeResolver.class)
    private final ChildBase child;

    public Parent(
            @JsonProperty("type") ParentType type,
            @JsonProperty("child") ChildBase child
    ) {
        this.type = type;
        this.child = child;
    }

    public ParentType type() {
        return type;
    }

    public ChildBase child() {
        return child;
    }

}

public static interface ChildBase {
}

public static record AggressiveChild(String someString) implements ChildBase {
}

public static record ChilledChild(String someString) implements ChildBase {
}

public static class ChildBaseByParentTypeResolver extends TypeIdResolverBase {

    private JavaType superType;

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

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

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        Class<?> subType = switch (id) {
            case "CHILLED" -> ChilledChild.class;
            case "AGGRESSIVE" -> AggressiveChild.class;
            default -> throw new IllegalArgumentException();
        };
        return context.constructSpecializedType(superType, subType);
    }

    @Override
    public String idFromValue(Object value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String idFromValueAndType(Object value, Class<?> suggestedType) {
        throw new UnsupportedOperationException();
    }
}

}

Expected behavior
Should work with records, too.
For now, using normal class as workaround.

Additional context
(none)