Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

valueToTree() doesn't work with WRAP_ROOT_VALUE #4859

Open
1 task done
stickfigure opened this issue Dec 19, 2024 · 12 comments
Open
1 task done

valueToTree() doesn't work with WRAP_ROOT_VALUE #4859

stickfigure opened this issue Dec 19, 2024 · 12 comments
Labels
to-evaluate Issue that has been received but not yet evaluated

Comments

@stickfigure
Copy link
Contributor

stickfigure commented Dec 19, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

If you configure these:

           .configure(SerializationFeature.WRAP_ROOT_VALUE, true)
           .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);

Then mapper.valueToTree() fails with an exception that looks like this ('Thing' is the type being written to a tree):

Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Root name ('Thing') does not match expected ('JsonNode') for type `com.fasterxml.jackson.databind.JsonNode`
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.fasterxml.jackson.databind.JsonNode["Thing"])
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at com.fasterxml.jackson.databind.DeserializationContext.reportPropertyInputMismatch(DeserializationContext.java:1802)
	at com.fasterxml.jackson.databind.DeserializationContext.reportPropertyInputMismatch(DeserializationContext.java:1818)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext._unwrapAndDeserialize(DefaultDeserializationContext.java:367)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:339)
	at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4881)
	at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:3145)
	at com.fasterxml.jackson.databind.ObjectMapper.valueToTree(ObjectMapper.java:3610)

Version Information

2.17.2

Reproduction

class WrappingObjectMapperTest {

    private record Thing(String foo) {}

    @Test
    void canConvertValuesToTrees() {
        final Thing thing = new Thing("bar");

        final ObjectMapper mapper = new ObjectMapper()
            .configure(SerializationFeature.WRAP_ROOT_VALUE, true)
            .configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);

        final JsonNode tree = mapper.valueToTree(thing);

        assertThat(tree.toString()).isEqualTo("{\"Thing\":{\"foo\":\"bar\"}}");
    }
}

Expected behavior

No response

Additional context

No response

@stickfigure stickfigure added the to-evaluate Issue that has been received but not yet evaluated label Dec 19, 2024
@JooHyukKim
Copy link
Member

One question, has ur usecase ever worked before? Wondering whether this is a regression or just uncovered case for those features

@cowtowncoder
Copy link
Member

I'd have to check as well: I think the idea would be that root wrapping/unwrapping should not be done for case of converting values, including valueToTree().
But it is definitely bit of a grey area.

@JooHyukKim
Copy link
Member

@stickfigure Small thing tho, in any case, modifying your reproduction to be decompiled version(plain Java without @Value) would be great! 👍🏼

@stickfigure
Copy link
Contributor Author

I'm pretty horrified by WRAP_ROOT_VALUE but someone in this company started with it a decade ago and I'm kinda stuck with it. I have no idea if valueToTree() ever worked in this context, I only inherited this codebase recently.

In the short run I've subclassed ObjectMapper and overridden valueToTree() with something approximately like this, which makes the method work the way I would expect:

return (T)normalNonWrappingObjectMapper.readTree(writeValueAsString(object));

I changed my sample to use record - sorry.

It's hard for me to imagine any situation where someone wouldn't expect valueToTree() to output the equivalent of writeValueAsString(). Seems like that should be an invariant.

@JooHyukKim
Copy link
Member

It's hard for me to imagine any situation where someone wouldn't expect valueToTree() to output the equivalent of writeValueAsString(). Seems like that should be an invariant.

If mapper.readTree(writeValueAsString) works, that's valid point in terms of intuition from naming itself. But then again, there may not be complete symmetry between plain POJO handling vs ObjectNode(or sorts).
TODO: verify mapper.readTree(writeValueAsString) works.

Lemme trying to read documentation (shared below) see what direction we should be heading.

image

@cowtowncoder
Copy link
Member

Yeah, JsonNode is expected to be faithful representation of content exactly as-is.
I may be misremembering aspect of WRAP_ROOT_VALUE not being enabled.. maybe I'll need to dig in to see.
(I vaguely remember something being problematic wrt wrap/unwrap having to be balanced but can't recall details now).

@cowtowncoder
Copy link
Member

Ok: wrapping is definitely disabled for convertValue():

    protected Object _convert(Object fromValue, JavaType toValueType)
        throws IllegalArgumentException
    {
        // inlined 'writeValue' with minor changes:
        // first: disable wrapping when writing
        final SerializationConfig config = getSerializationConfig().without(SerializationFeature.WRAP_ROOT_VALUE);
        ...

@cowtowncoder
Copy link
Member

cowtowncoder commented Dec 20, 2024

Ah ha. There is some prior art -> #4047.

Funny I didn't even remember this, despite @JooHyukKim and I discussing it just a year ago. :)

@cowtowncoder
Copy link
Member

Ah. Actually, this might be working as expected -- there is mismatch wrt expected "Root name": on serialization simple class name ("Thing") is used, and expectation is that same is the case when reading (that is, Root Name expectation is set by simple name, JsonNode).

The same thing would occur if explicit ObjectMapper.writeValue() + ObjectMapper.readTree() was used -- although there it'd be possible to use something like:

mapper.reader().withRootName("Thing").readTree(json);

to override expected root name.
In fact, I think that is probably what you need here.

Or... if you want to keep "Thing" as wrapper, simply leave DeserializationFeature.UNWRAP_ROOT_VALUE as false (or set explicitly).

I am not sure there is anything to be done on Jackson side.

@JooHyukKim
Copy link
Member

Funny I didn't even remember this, despite @JooHyukKim and I discussing it just a year ago. :)

Strange it felt so new to me 😂.

@JooHyukKim
Copy link
Member

In this case, we may close this as working-as-expected and try see what we can do on documentation side.

@cowtowncoder
Copy link
Member

Agreed. Solution for @stickfigure is, I think, not enabling UNWRAP_ROOT_VALUE (or disabling it for these reads)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to-evaluate Issue that has been received but not yet evaluated
Projects
None yet
Development

No branches or pull requests

3 participants