Skip to content

JsonTypeInfo.As.EXTERNAL_PROPERTY does not work with record wrappers #3342

@yamass

Description

@yamass

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    RecordIssue related to JDK17 java.lang.Record support

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions