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("me@me.com"),
"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\":\"me@me.com\"");
}
}
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":"me@me.com"},"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":"me@me.com"},"message":"Sample Message"}
Expected behavior
The JSON output should be the same in both cases.
Additional context
No response
Comment From: pjfanning
Can you provide the definition of the MessageWrapper class?
Comment From: wimdeblauwe
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";
}
Comment From: cowtowncoder
Probably makes no difference, but there is now 2.20.0 release to try out.
Comment From: cowtowncoder
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.