FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.51k stars 1.37k forks source link

Map Key interface not serialized with both type serializer or keyUsing #3119

Open lorthirk opened 3 years ago

lorthirk commented 3 years ago

Describe the bug

In my application I have a Map<KapuaId, Boolean> where KapuaId is an interface, and has its own serializer (since I also use this type in other parts as a standalone object):

public class KapuaIdSerializer extends JsonSerializer<KapuaId> {

    @Override
    public void serialize(KapuaId value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(value.toCompactId());
    }

}

So, I first thought that registering the mixin for the standalone type would have also worked when serializing the Map<KapuaId, Boolean>:

objectMapper = new ObjectMapper();
objectMapper.addMixIn(KapuaId.class, KapuaIdMixin.class);
@JsonDeserialize(using = KapuaIdDeserializer.class)
@JsonSerialize(using = KapuaIdSerializer.class)
public interface KapuaIdMixin { }

But unfortunately it's not, since the toString() of the concrete type is returning:

{
  "1": null,
  "5667511473135966782": false,
  "8466406404189434576": false
}

So I built a container object, where I used @JsonValue and @JsonSerialize(keyUsing = KapuaIdSerializer.class), but nothing changed:

public class IsJobRunningMapResponse {

    private final Map<KapuaId, Boolean> map;

    public IsJobRunningMapResponse(Map<KapuaId, Boolean> map) {
        this.map = map;
    }

    @JsonValue
    @JsonSerialize(keyUsing = KapuaIdSerializer.class)
    public Map<KapuaId, Boolean> getMap() {
        return map;
    }

}

But the result was the same. In the end I had to use a Converter for the whole container type, basically duplicating what KapuaIdSerializer was already doing:

public class IsJobRunningMapResponseSerializer extends JsonSerializer<IsJobRunningMapResponse> {

    @Override
    public void serialize(IsJobRunningMapResponse value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeStartObject();
        for (Map.Entry<KapuaId, Boolean> entry : value.getMap().entrySet()) {
            if (entry.getValue() == null) {
                gen.writeNullField(entry.getKey().toCompactId());
            } else {
                gen.writeBooleanField(entry.getKey().toCompactId(), entry.getValue());
            }
        }
        gen.writeEndObject();
    }

And in the end the response was the one I expected, with KapuaId serialized correctly:

// KapuaId value coming from KapuaId.toCompactId()

{
  "AQ": null,
  "TqcLqDS4Wj4": false,
  "dX63xIXNstA": false
}

So... I found a workaround, but I think that something is not ok with the key type serialization.

Version information 2.12.2

To Reproduce If you have a way to reproduce this with:

Steps reproduced above. Full repository at https://github.com/lorthirk/kapua/tree/feature-multipleJobIsRunning/job-engine/app/resources/src/main/java/org/eclipse/kapua/job/engine/app

Expected behavior KapuaId should have been serialized with its own serializer, or at least keyUsing should not have been ignored

Additional context Using together with Jersey 2.23.2

cowtowncoder commented 3 years ago

It does seem reasonable that one could combine @JsonValue with serializer configuration so I will leave this issue open to investigate and hopefully fix that use case.

You are right in observing that registering "regular" (value) serializer for a type does not make it be used for Map key use case: this is by design and key (de)serializers registered separately. This because method for writing map key (generator.writeFieldName()) is different from writing regular string value (generator.writeString() (and ditto for reading side).

But you should be able to use keyUsing on mix-in or, probably better, register key (de)serializer using module. There is also a relatively new annotation, @JsonKey that you can use instead of @JsonValue for annotating field or method (of key type) to use as serialization.

So, for mix-in, you would want to add @JsonKey on method toCompactId(), I think? Note, too, that if you do NOT specify @JsonKey, @JsonValue is considered the default serialization for Map keys as well as value serialization. So if this method works for both, just use @JsonValue.