Generics using wildcard not correctly resolved
Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
I opened a Spring Boot issue, but it seems the issue is Jackson related. See https://github.com/spring-projects/spring-boot/issues/46994#issuecomment-3236302850 for the full details and a reproducer.
Version Information
2.19.2
Reproduction
package com.example.jsontesterbug;
import static org.assertj.core.api.Assertions.assertThat;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
class MessageWrapperTest {
private MessageWrapper<?> wildcardWrapper;
private MessageWrapper<EmailSettings> specificWrapper;
private ObjectMapper objectMapper = new ObjectMapper();
@Test
void wildcardWrapper() throws NoSuchFieldException, SecurityException, JsonProcessingException {
serializeWithTypeFromField("wildcardWrapper");
}
@Test
void specificWrapper() throws NoSuchFieldException, SecurityException, JsonProcessingException {
serializeWithTypeFromField("specificWrapper");
}
private void serializeWithTypeFromField(String field) throws NoSuchFieldException, SecurityException, JsonProcessingException {
MessageWrapper<EmailSettings> wrapper = new MessageWrapper<>(new EmailSettings("[email protected]"),
"Sample Message");
Type genericType = MessageWrapperTest.class.getDeclaredField(field).getGenericType();
TypeVariable<?>[] typeParameters = ((Class<?>)((ParameterizedType)genericType).getRawType()).getTypeParameters();
Type[] bounds = typeParameters[0].getBounds();
System.out.println(field);
System.out.println(" Generic type: " + genericType);
System.out.println(" Bounds: " + Arrays.toString(bounds));
JavaType jacksonType = this.objectMapper.constructType(genericType);
System.out.println(" Jackson type: " + jacksonType);
String json = this.objectMapper.writerFor(jacksonType).writeValueAsString(wrapper);
System.out.println(" JSON: " + json);
assertThat(json).contains("\"type\":\"EMAIL\"");
assertThat(json).contains("\"email\":\"[email protected]\"");
}
}
Running this gives this output:
wildcardWrapper
Generic type: com.example.jsontesterbug.MessageWrapper<?>
Bounds: [interface com.example.jsontesterbug.Settings]
Jackson type: [simple type, class com.example.jsontesterbug.MessageWrapper<java.lang.Object>]
JSON: {"settings":{"email":"[email protected]"},"message":"Sample Message"}
specificWrapper
Generic type: com.example.jsontesterbug.MessageWrapper<com.example.jsontesterbug.EmailSettings>
Bounds: [interface com.example.jsontesterbug.Settings]
Jackson type: [simple type, class com.example.jsontesterbug.MessageWrapper<com.example.jsontesterbug.EmailSettings>]
JSON: {"settings":{"type":"EMAIL","email":"[email protected]"},"message":"Sample Message"}
Expected behavior
The JSON output should be the same in both cases.
Additional context
No response
Can you provide the definition of the MessageWrapper class?
You can view it in the zip file of the Spring Boot issue as well, but here it is:
public record MessageWrapper<T extends Settings>(T settings, String message) {
}
This is the Settings interface:
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
@JsonTypeInfo(use = Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(
value = EmailSettings.class,
name = EmailSettings.TYPE),
@JsonSubTypes.Type(
value = PhoneSettings.class,
name = PhoneSettings.TYPE),
})
public interface Settings {
}
With EmailSettings:
public record EmailSettings(String email) implements Settings {
public static final String TYPE = "EMAIL";
}
and PhoneSettings:
public record PhoneSettings(String phoneNumber) implements Settings {
public static final String TYPE = "PHONE";
}
Probably makes no difference, but there is now 2.20.0 release to try out.
This:
Jackson type: [simple type, class com.example.jsontesterbug.MessageWrapper<java.lang.Object>]
shows underlying problem: type is resolved as MessageWrapper<?> and that's why @JsonTypeInfo does not take effect. This is from getting
private MessageWrapper<?> wildcardWrapper;
method signature, which for some reason does not handle bounds for MessageWrapper type parameter.
Test could probably be simplified to just that type resolution problem.
@yawkat might be familiar with this problem actually.