ReadTreeAsValueParentPath1803Test.java

package tools.jackson.databind.node;

import org.junit.jupiter.api.Test;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;

// [databind#1803]: when a custom deserializer converts a buffered subtree via
// `ctxt.readTreeAsValue(...)`, the resulting parser should report the FULL document
// path (parent context linked) rather than one that restarts at the subtree.
public class ReadTreeAsValueParentPath1803Test
    extends DatabindTestUtil
{
    static class Outer {
        @JsonDeserialize(using = InnerDeserializer.class)
        public Inner inner;
    }

    static class Inner {
        public String pathSeenByDeserializer;
        public int x;
    }

    // Two-phase deserializer: read the value as a tree, then convert it back via
    // `readTreeAsValue`, capturing the path the nested deserialization observes.
    static class InnerDeserializer extends ValueDeserializer<Inner> {
        @Override
        public Inner deserialize(JsonParser p, DeserializationContext ctxt) {
            JsonNode node = ctxt.readTree(p);
            Inner result = ctxt.readTreeAsValue(node, InnerImpl.class);
            return result;
        }
    }

    // Distinct type to avoid recursive use of InnerDeserializer; its own deserializer
    // records the path it sees while being read from the tree-traversing parser.
    @JsonDeserialize(using = InnerImplDeserializer.class)
    static class InnerImpl extends Inner { }

    static class InnerImplDeserializer extends ValueDeserializer<InnerImpl> {
        @Override
        public InnerImpl deserialize(JsonParser p, DeserializationContext ctxt) {
            InnerImpl result = new InnerImpl();
            // Record path at the point the Object value starts being read
            result.pathSeenByDeserializer = p.streamReadContext().pathAsPointer().toString();
            // consume the object so parsing completes cleanly
            for (JsonToken t = p.currentToken(); t != null && t != JsonToken.END_OBJECT; t = p.nextToken()) {
                if (t == JsonToken.PROPERTY_NAME && "x".equals(p.currentName())) {
                    p.nextToken();
                    result.x = p.getIntValue();
                }
            }
            return result;
        }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    void readTreeAsValueSeesFullParentPath() throws Exception {
        Outer outer = MAPPER.readValue("{\"inner\":{\"x\":42}}", Outer.class);
        assertEquals(42, outer.inner.x);
        // With parent context linked, the nested deserializer sees "/inner" prefix.
        assertEquals("/inner", outer.inner.pathSeenByDeserializer);
    }
}