Skip to content

Fix improper (De)Serialization given both @JsonTypeInfo and @JsonValue#5850

Open
blacelle wants to merge 3 commits intoFasterXML:3.xfrom
solven-eu:jsoninfotype/jsonvalue
Open

Fix improper (De)Serialization given both @JsonTypeInfo and @JsonValue#5850
blacelle wants to merge 3 commits intoFasterXML:3.xfrom
solven-eu:jsoninfotype/jsonvalue

Conversation

@blacelle
Copy link
Copy Markdown
Contributor

@blacelle blacelle commented Mar 28, 2026

This follow-up #5035

I could open a dedicated issue, but I felt fine just opening a PR for now given I have code (UT and draft for fix).

This case refers to the interaction of @JsonTypeInfo and @JsonValue.

  • Expectation: Typically, in case of @JsonValue, I argue @JsonTypeInfo should not generally not trigger the type.
  • Observation: In current state of the library, it leads to wrapping the type and the @JsonValue field in an array.

Unit-tests demonstrates 2 cases which relates to the similar functional cases, but they seem to refer to different Jackson classes:

  • A class with @JsonValue implements some interface with @JsonTypeInfo
  • An enum (without @JsonValue but with similar semantic) implements some interface with @JsonTypeInfo

I reach good (in my perspective) behavior for the @JsonValue case by forcing JsonValueSerializer.forceTypeInformation when serializer has a _accessor (which I interpret as a signal there is a @JsonValue) .

The enum is not benefitting from @JsonValue case as it seems there is no similar forceTypeInformation around enum.

Some stack relating to the enum case (I tried to find a spot to disable typeSerialization along this stack):

AsPropertyTypeSerializer.<init>(TypeIdResolver, BeanProperty, String) line: 24	
StdTypeResolverBuilder.buildTypeSerializer(SerializationContext, JavaType, Collection<NamedType>) line: 157	
TypeResolverProvider.findTypeSerializer(SerializationContext, JavaType, AnnotatedClass) line: 69	
SerializationContextExt$Impl(SerializationContext).findTypeSerializer(JavaType, AnnotatedClass) line: 842	
SerializationContextExt$Impl(SerializationContext).findTypeSerializer(JavaType) line: 830	
SerializationContextExt$Impl(SerializationContext).findTypedValueSerializer(Class<?>, boolean) line: 593	
SerializationContextExt$Impl(SerializationContextExt).serializeValue(JsonGenerator, Object) line: 297	
ObjectMapper._configAndWriteValue(SerializationContextExt, JsonGenerator, Object) line: 1901	
ObjectMapper.writeValueAsString(Object) line: 1845	
TestJsonTypeInfoJsonValue.testEnum_Custom_string() line: 201	

@blacelle
Copy link
Copy Markdown
Contributor Author

blacelle commented Mar 28, 2026

Failing tests:

Error:    TestPropertyTypeInfo.testSimpleArrayField:163 expected: <tools.jackson.databind.jsontype.TestPropertyTypeInfo.BooleanValue> but was: <java.lang.Boolean>
Error:    TestPropertyTypeInfo.testSimpleListMethod:143 expected: <tools.jackson.databind.jsontype.TestPropertyTypeInfo.BooleanValue> but was: <java.lang.Boolean>
Error:    TestPropertyTypeInfo.testSimpleMapMethod:210 expected: <tools.jackson.databind.jsontype.TestPropertyTypeInfo.BooleanValue> but was: <java.lang.Boolean>

relies on @JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.WRAPPER_ARRAY).

ExternalTypeIdTest.testDoubleMetadata:830 Serialized json not equivalent ==> expected: <{"metadata":[{"key":"num","value":1234.25,"@type":"doubleValue"}]}> but was: <{"metadata":[{"key":"num","value":1234.25}]}>

relies on @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.EXTERNAL_PROPERTY).

  • JsonTypeInfo.As.WRAPPER_ARRAY calls for explicitely requesting a wrapping array.
  • While my case goes for include = JsonTypeInfo.As.PROPERTY,. I argue that I'm happy, with JsonTypeInfo.As.PROPERTY, to get a type property if the @JsonValue is an Object. But I do not like current behavior wrapping as an array.
  • (I feel like my case is different from these existing cases. Hence my current fix is broken).

I can not find in JsonValueSerializer.createContextual how to get the @JsonTypeInfo properties, especially to consider the nclude=JsonTypeInfo.As.WRAPPER_ARRAY. It suggests I'm rather looking for something around JsonValueSerializer.isNaturalTypeWithStdHandling as:

  • my main issue is to get the @JsonValue value wrapped in an array.
  • if such value was a complex object, I guess the type would be added as a property.
  • In case of a String, it is still wrapped as an array.
  • Given JsonValueSerializer.isNaturalTypeWithStdHandling, I feel lost, as I would expect String to be considered primitive hence typeInformation would not need to be forced.

Or maybe, what I'm looking for is a way to prevent AsPropertyTypeSerializer to fallback on JsonTypeInfo.As.WRAPPER_ARRAY if the configuration requests JsonTypeInfo.As.PROPERTY (and/or in case of natural types).

*/
boolean forceTypeInformation;
if (_accessor == null) {
// Why do we forceTypeInformation if the type is natural? Shouldn't it be the contrary?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that sounds odd. It must be due to fundamental problem with @JsonValue handling usually changing type of value being serialized (POJO) into intermediate type; so assumption here being former is not "natural", but if latter is, we want to determine type serialization requirement on former.

Like @JsonValue annotated method returning String, for some POJO.

@cowtowncoder
Copy link
Copy Markdown
Member

There is basic contradiction between @JsonValue and @JsonTypeInfo, and I almost feel like combo should not be allowed (and regret not trying to block it earlier)

Problem is the "translation" @JsonValue does -- where as base type for @JsonTypeInfo really must be for actual type of POJO on which @JsonValue is added, actual value being serialized often has no relation to that type.

And so @JsonTypeInfo operates usually on type unrelated to Value type. This is just... wrong.

So one possibility would be to start failing cases like this.

If so, we'd likely need MapperFeature to enable/disable failing, to support legacy cases.

But not that if the two are supported, handling of @JsonTypeInfo -- when on ACTUAL type, not type returned with @JsonValue annotation -- MUST be done. But conversely, when serializing @JsonValue annotated type, should be suppressed (or, lead to failure)
Maybe this is what you are suggesting?

@blacelle
Copy link
Copy Markdown
Contributor Author

First, let me reword my use-case; why I go for both @JsonInfoType and @JsonValue. (As I'd like to succeed implementing my case, not just failing ; this is not just a plain here what I found suspicious case).

  1. I rely on @JsonInfoType for dynamic typing. I consider some interface, and may receive various types, that I would not know in advance (developping a library) .
  2. I provide a set of implementations for given interface. Amongst which some are very simple.
  3. For the default case, which is often a plain String (or an enum), I would like the json to hold only a plain String (e.g. 'xxx') and neither {'type': 'mostCommonClass', 'value': 'xxx'} nor {'type': 'mostCommonClass', 'value': {'commonField': 'xxx'}}

So:

  • @JsonInfoType is a strong requirement (but you may hint me towards a different option to help managing dynamic typing)
  • @JsonValue is cherry on the cake, to improve/simplify the option used in 95% cases.

This is much related with #5705, which address this need with custom ValueSerializerModifier. Given there is a way to go around, I would say this is not a big deal. Still, I guess I'm curious if I could get it working more natively.


But not that if the two are supported, handling of @JsonTypeInfo -- when on ACTUAL type, not type returned with @jsonvalue annotation -- MUST be done. But conversely, when serializing @jsonvalue annotated type, should be suppressed (or, lead to failure)
Maybe this is what you are suggesting?

I suggest something in this vein, yes. TestPropertyTypeInfo.testSimpleListMethod currently specify (by unit-testing) this rule may not always apply (e.g. on JsonTypeInfo.As.WRAPPER_ARRAY, one may argue we should always wrap, as everything is wrappable in an array, while JsonTypeInfo.As.PROPERTY may be optional as not everything (e.g. primitive json types) can have properties).

@cowtowncoder
Copy link
Copy Markdown
Member

cowtowncoder commented Mar 30, 2026

Ok, put another way, the problem is that unlike for regular usage, where both ValueSerializer and TypeSerializer (when using polymorphic handling) operate on same value (and hence same type; although for latter it is "base type" so may be a super type of type ValueSerializer uses), @JsonValue breaks this so that ValueSerializer deals with possibly (and, usually) unrelated type.

It may not be impossible to support but is hard to reason about within code. ValueSerializer method:

    public void serializeWithType(T value, JsonGenerator gen, SerializationContext ctxt, TypeSerializer typeSer);

of, say:

@JsonTypeInfo
class MyValue {
   @JsonValue
   public String toJson() {
      return stringValue;
  }
}

will need TypeSerializer for MyValue, but ValueSerializer for String; and call is on latter, delegating to former.
Deserialization side is not similarly affected since @JsonValue is not used for deserialization (except for a bit for Enums in some cases).

This case also illustrated challenges on "natural" types -- so, normally when serializing "String as String", no type info should be included, f.ex on:

class Wrapper {
  @JsonTypeInfo(...)
  public Object value;
}

no type id would be include for java.lang.String values (and similarly with Default Typing). So StringSerializer does this:

    @Override
    public final void serializeWithType(Object value, JsonGenerator gen, SerializationContext provider,
            TypeSerializer typeSer)
    {
        // no type info, just regular serialization
        gen.writeString((String) value);
    }

but this is now a problem for @JsonValue case -- where delegation to TypeSerializer SHOULD occur.
Because logical value being serialized is not String: we are just serializing String for convenience instead of MyValue instance (sort of pre-serialized).
But whereas "natural" String needs no type id (Deserializer knows to map it without type id), surrogate case for @MyValue absolutely does (because we do not want to deserialize as String but as MyValue and hence must have type id to map).

Changing StringSerializer to delegate to TypeSerializer breaks 3 unit tests which is not a lot (but suggests problems of course) so maybe it should be done -- but only after careful consideration...

@blacelle
Copy link
Copy Markdown
Contributor Author

blacelle commented Mar 30, 2026

I guess I get your point. TO put it in others words: with @JsonValue, it may be ambiguous to which type does @JsonTypeInfo should apply: the embedding type or the embedded type.

In my case (and in my original understanding), the question was irrelevant as @JsonValuewas always on a natural type (so I expect to type in the output), and I used @JsonTypeInfo.defaultImpl to force deserialization with the embedding type. (irrelevant in the sense I expected no type info in the output).

With my current understanding, I just see an ambiguity.

But whereas "natural" String needs no type id (Deserializer knows to map it without type id), surrogate case for @MyValue absolutely does (because we do not want to deserialize as String but as MyValue and hence must have type id to map).

I guess you consider @JsonTypeInfo should output the embedding type, even if the embedded value is a primitive.

Changing StringSerializer to delegate to TypeSerializer breaks 3 unit tests which is not a lot (but suggests problems of course) so maybe it should be done -- but only after careful consideration...

I'll have a look.


Regarding the ambiguity, I wonder if one way forward is we could disable @JsonTypeInfo for some types. In my case, I would disable it for the class with a @JsonValue. e.g. what if I go for @JsonTypeInfo.As = NONE for the type with @JsonValue? (I suppose it does not work in current code base, but it may help around this matter).

@cowtowncoder
Copy link
Copy Markdown
Member

cowtowncoder commented Mar 30, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants