Describe the bug
When serializing a map key, if the key's type uses @JsonValue on one of its attributes, and if that attribute's type uses @JsonValue on one of its own attributes, the second @JsonValue is ignored, and toString() is used instead.
Version information
2.10.0
To Reproduce
class Inner {
@JsonValue
String string;
Inner(String string) {
this.string = string;
}
public String toString() {
return "Inner(String="+this.string+")";
}
}
class Outer {
@JsonValue
Inner inner;
Outer(Inner inner) {
this.inner = inner;
}
}
public void test() throws Exception {
Outer outer = new Outer(new Inner("key"));
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(outer)); // outputs "key", as expected
System.out.println(mapper.writeValueAsString(Collections.singletonMap(outer,"value"))); // outputs {"Inner(String=key)":"value"}, expected {"key":"value"}
}
Correct: @JsonValue currently only affects serialization of _values_, not (Map) keys.
The reason for this is mostly duality of key and value handling: at low level actual (de)serializers need to work bit differently both since keys can only be Strings (at least in JSON; many other formats allow different types), and since streaming API (JsonParser, JsonGenerator) handle key token different from String value token.
Currently one has to register separate key serializer either using Module (like SimpleModule), or using @JsonSerialize(keyUsing=) on property or key class itself.
Having said that, I can see why it'd be really nice if @JsonValue did also work for Map keys.
I have slight concern as to whether this could cause problems for some users (if they wanted to register different key serializer from annotations -- annotations typically have precedence over (de)serializer registrations), so it might be safest to add something like
@JsonKey OR @JsonUseAsKey
annotation. That would often mean having to add both @JsonValue and the new annotation.
Alternatively I guess it would be possible to add a property for @JsonValue that would allow optional exclusion for use for key (for unlikely case that only true "value" serialization would use annotation field/method).
It could be best to use another annotation indeed, like @JsonKey, because you could also want to serialize an object differently if it's a key or a value (since, as you said, in JSON, keys must be strings, unlike values).
I just want to add that, currently, @JsonValue does affect serialization of keys, since, in my example, the @JsonValue on Outer.inner is not ignored ; it's the @JsonValue on Inner.string that is ignored.
Good point wrt different annotation for key/value aspect, somehow missed that (I think I had that in mind in the past).
I'll have to see how @JsonValue is used, then; I was thinking that it might be used for Enums but not other types. But perhaps I have forgotten about a feature I added at some point...
Adding a separate annotation would make sense, regardless, I think -- but obviously need to consider existing handling as users may be relying on that.
And I agree that if chaining works for values, it should work for keys as well.
Yep, for example : while serializing a map key, if there is a @JsonKey, use it (with chaining), else if there is a @JsonValue, use it (with chaining too), else use toString()?
I've started working on this.
class Inner {
@JsonKey
@JsonValue
String string;
Inner(String string) {
this.string = string;
}
public String toString() {
return "Inner(String="+this.string+")";
}
}
class Outer {
@JsonValue
Inner inner;
Outer(Inner inner) {
this.inner = inner;
}
}
If we only put @JsonValue on the Outer class (and not @JsonKey), would you expect it to trickle down and read the @JsonKey value on the inner class? Or would the requirement be to have @JsonKey on Outer and Inner?
@Anusien Interesting... this gets quite tricky, wrt overlap of annotations, precedence.
I think that it will be simpler and more reliable to keep @JsonValue and @JsonKey separate, and that would, I think, require duplication of annotation at both levels.
Conversely if handling of the two was coupled it could cover more cases with fewer annotations but would probably have weird edge cases and more complicated handling internally (to try to combine intended logic).
Does this make sense?
Just realized there's the original issue, #47, that gives more context.
Most helpful comment
I've started working on this.