Skip to content

Serializing a Map<K, V> field uses declared type for key serialization rather than key instance type #4736

@emilyy-dev

Description

@emilyy-dev

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

When serializing a field of type Map<K, V>, Jackson will inspect the declared K type for serialization annotations, rather than the individual entries key's instance type. This creates issues where K is a supertype and the map holds subtypes of K (K') that do specify serialization annotations.

Values are serialized as expected and the instance type is used for annotation introspection.

Version Information

2.18.0

Reproduction

public final class Main {
  public static void main(final String[] args) throws IOException {
    final var list = new IntOrStringList(List.of(new IntOrString.StringValue("foo"), new IntOrString.IntValue(123)));
    final var map = new IntOrStringMap(Map.of(new IntOrString.StringValue("bar"), new IntOrString.IntValue(456)));

    final var mapper = new JsonMapper();
    System.out.println(mapper.writeValueAsString(list));
    System.out.println(mapper.writeValueAsString(map));
    System.out.println(mapper.writeValueAsString(Map.of(new IntOrString.StringValue("baz"), new IntOrString.IntValue(789))));
  }

  public record IntOrStringList(List<IntOrString> values) {
  }

  public record IntOrStringMap(Map<IntOrString, IntOrString> values) {
  }

  public sealed interface IntOrString {

    @JsonKey(false) String forMapKeySerialization();

    record IntValue(@JsonValue int value) implements IntOrString {

      @Override
      public String forMapKeySerialization() {
        return String.valueOf(this.value);
      }
    }

    record StringValue(@JsonValue String value) implements IntOrString {

      @Override
      public String forMapKeySerialization() {
        return this.value;
      }
    }
  }
}

Expected behavior

In the reproducer above, the first println prints {"values":["foo",123]}, the second println prints {"values":{"StringValue[value=bar]":456}}, using the default toString() for the key. The third println is included to display that the issue is when serializing a map field, not a map value directly as that third prints {"baz":789} which is the kind of serialization behavior I'd expect for the second case as well.

Enabling the @JsonKey in the interface's method makes it behave how I'd expect, and the second println now prints {"values":{"bar":456}}. The annotation's presence when disabled doesn't play a role, as it doesn't behave any differently when it isn't there (as expected).

Additional context

The most relevant issue I could find is #3119 given the inheritance setting, but explicitly specifying @JsonSerialize(keyUsing = ...) behaves correctly so I ruled that out, but they might be related in a way I can't see.

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.18Issues planned at 2.18 or later

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions