jackson-databind
jackson-databind copied to clipboard
JsonTypeInfo.As.EXTERNAL_PROPERTY does not work with record wrappers
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)
Has there been any investigation into this? It's a bit annoying to have to use one class file when all of the rest of my stuff is using records.
I'm doing this a bit differently (using the JsonTypeInfo and JsonSubTypes annotations) so here is a test case demonstrating how using records fails while using a class works:
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class JacksonRecordTypeInfoTest {
// This test works since it's using the ParentClass class type
@Test
void testJsonSubTypesUsingClass() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
ParentClass parent = objectMapper.readValue("""
{"type": "CHILLED", "child": {}}
""", ParentClass.class);
Assertions.assertTrue(parent.child instanceof ChilledChild);
}
// This test fails since it uses the ParentRecord record type
@Test
void testJsonSubTypesUsingRecord() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
ParentRecord parent = objectMapper.readValue("""
{"type": "CHILLED", "child": {}}
""", ParentRecord.class);
Assertions.assertTrue(parent.child instanceof ChilledChild);
}
public enum ParentType {
CHILLED,
AGGRESSIVE
}
public static class ParentClass
{
ParentType type;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = AggressiveChild.class, name = "AGGRESSIVE"),
@JsonSubTypes.Type(value = ChilledChild.class, name = "CHILLED")
})
ChildBase child;
}
public record ParentRecord(
ParentType type,
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = AggressiveChild.class, name = "AGGRESSIVE"),
@JsonSubTypes.Type(value = ChilledChild.class, name = "CHILLED")
})
ChildBase child
){}
public interface ChildBase {
}
public record AggressiveChild(String someString) implements ChildBase {
}
public record ChilledChild(String someString) implements ChildBase {
}
}
I have not had any time to work on this unfortunately. I am not sure if this is due to general issues with Records (related to introspection of Constructors wrt other properties), or specifically because Records cannot be made to work with EXTERNAL_PROPERTY. The reason it might be latter is that due to immutability, the way this feature usually works may well not be available -- since passing external property type ids via Creator methods itself is not supported (and for Records they are the only mechanism).
I have been hoping to rewrite property introspection for Jackson 2.14, and if I ever find time to do that, it would help unravel a set of record-related issues, including this one.