Skip to content

Generics using wildcard not correctly resolved #5285

Closed

Description

@wimdeblauwe

Search before asking

  • 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 spring-projects/spring-boot#46994 (comment) 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

Activity

  1. pjfanning commented on Aug 29, 2025

    @pjfanning
    Member

    Can you provide the definition of the MessageWrapper class?

  2. wimdeblauwe commented on Aug 29, 2025

    @wimdeblauwe
    Author

    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";
    }
    
  3. cowtowncoder commented on Sep 6, 2025

    @cowtowncoder
    Member

    Probably makes no difference, but there is now 2.20.0 release to try out.

  4. cowtowncoder commented on Sep 6, 2025

    @cowtowncoder
    Member

    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.

  5. added
    2.19Issues planned at 2.19 or later
    and removed
    to-evaluateIssue that has been received but not yet evaluated
    on Sep 19, 2025
  6. added a commit that references this issue on Feb 4, 2026
    03e2df4
  7. added and removed
    2.19Issues planned at 2.19 or later
    on Feb 4, 2026
  8. added this to the 3.1.0 milestone on Feb 4, 2026
  9. cowtowncoder commented on Feb 4, 2026

    @cowtowncoder
    Member

    Fixed in 3.x for 3.1.0 (post -rc1)

  10. added a commit that references this issue on Feb 4, 2026
    bb0cc32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions