Skip to content

Inner classes that get their generic information from an outer class lose type information #3288

Open

Description

@jackwickham

Describe the bug
If a non-static inner class uses generics from the outer class, that generic information is lost when serializing.

Version information
2.12.5

To Reproduce

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

public final class JacksonPoC {

    @JsonTypeInfo(use = Id.NAME)
    @JsonSubTypes({@JsonSubTypes.Type(value = ConcreteValue.class, name = "concrete-value")})
    private interface AbstractValue {}

    private static class ConcreteValue implements AbstractValue {}

    public interface Wrapper<T> {
        T getValue();
    }

    private static final class Outer<T> {
        private final class Inner implements Wrapper<T> {
            private final T value;

            Inner(T value) {
                this.value = value;
            }

            @Override
            public T getValue() {
                return value;
            }
        }

        Inner create(T value) {
            return new Inner(value);
        }
    }

    private static final class Direct<T> implements Wrapper<T> {
        private final T value;

        Direct(T value) {
            this.value = value;
        }

        @Override
        public T getValue() {
            return value;
        }
    }

    private static final class Container {
        private final Wrapper<AbstractValue> wrapper;

        Container(Wrapper<AbstractValue> wrapper) {
            this.wrapper = wrapper;
        }

        public Wrapper<AbstractValue> getWrapper() {
            return wrapper;
        }
    }

    private static final ObjectMapper mapper = new ObjectMapper();

    @Test
    void direct() throws Exception {
        // This test passes because the generic information is present on the class directly
        Container object = new Container(new Direct<>(new ConcreteValue()));
        assertThat(mapper.writeValueAsString(object))
                .isEqualTo("{\"wrapper\":{\"value\":{\"@type\":\"concrete-value\"}}}");
    }

    @Test
    void indirect() throws Exception {
        // This test fails (the @type part is omitted) because the `AbstractValue` information is lost
        Container object = new Container(new Outer<AbstractValue>().create(new ConcreteValue()));
        assertThat(mapper.writeValueAsString(object))
                .isEqualTo("{\"wrapper\":{\"value\":{\"@type\":\"concrete-value\"}}}");
    }
}

Additional context
The example above does feel quite contrived, but I encountered this in practice with Guava's Multimaps.asMap on a SetMultimap, because the resulting class AbstractMapBasedMultimap.WrappedSet inherits the generic information from the parent.

When debugging this, I got as far as the short-circuit in

, but I'm not sure whether simply removing that is sufficient to resolve this.

Activity

  1. removed
    to-evaluateIssue that has been received but not yet evaluated
    on Apr 8, 2026
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions