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

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

Closed
yamass opened this issue Dec 7, 2021 · 2 comments · Fixed by #3724
Closed

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

yamass opened this issue Dec 7, 2021 · 2 comments · Fixed by #3724
Labels
Record Issue related to JDK17 java.lang.Record support
Milestone

Comments

@yamass
Copy link

yamass commented Dec 7, 2021

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)

@yamass yamass added the to-evaluate Issue that has been received but not yet evaluated label Dec 7, 2021
@StFS
Copy link

StFS commented Apr 12, 2022

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 {
    }
}

@cowtowncoder cowtowncoder added Record Issue related to JDK17 java.lang.Record support and removed to-evaluate Issue that has been received but not yet evaluated labels Apr 19, 2022
@cowtowncoder
Copy link
Member

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.

@cowtowncoder cowtowncoder added this to the 2.15.0 milestone Jan 14, 2023
@cowtowncoder cowtowncoder changed the title JsonTypeInfo.As.EXTERNAL_PROPERTY does not work with record wrappers JsonTypeInfo.As.EXTERNAL_PROPERTY does not work with record wrappers Jan 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Record Issue related to JDK17 java.lang.Record support
Projects
None yet
3 participants