ParsingContextExtTypeId2747Test.java

package tools.jackson.databind.jsontype.ext;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;

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;

class ParsingContextExtTypeId2747Test extends DatabindTestUtil {
    static class Wrapper {
        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type",
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
        public Tag wrapped;

        public String type;
    }

    // Creator-based wrapper: value flows through `ExternalTypeHandler.complete(p, ctxt, buffer, creator)`
    // which uses `_deserialize` (not `_deserializeAndSet`).
    static class CreatorWrapper {
        final String type;

        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type",
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
        @JsonSubTypes(@JsonSubTypes.Type(Location.class))
        final Tag wrapped;

        @JsonCreator
        CreatorWrapper(@JsonProperty("type") String type,
                @JsonProperty("wrapped") Tag wrapped) {
            this.type = type;
            this.wrapped = wrapped;
        }
    }

    // defaultImpl wrapper: when type-id property is missing, complete() resolves to
    // the default type via `extProp.getDefaultTypeId(ctxt)` and feeds that id through
    // the same fast path.
    static class DefaultImplWrapper {
        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type",
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
                defaultImpl = Location.class)
        public Tag wrapped;

        public String type;
    }

    @JsonSubTypes(@JsonSubTypes.Type(Location.class))
    interface Tag {
    }

    @JsonTypeName("location")
    @JsonDeserialize(using = LocationDeserializer.class)
    static class Location implements Tag {
        String value;

        protected Location() {
        }

        Location(String v) {
            value = v;
        }
    }

    static class LocationDeserializer extends ValueDeserializer<Location>
    {
        @Override
        public Location deserialize(JsonParser p, DeserializationContext ctxt)
        {
            p.skipChildren();
            return new Location(getCurrentLocationAsString(p));
        }
    }

    static String getCurrentLocationAsString(JsonParser p)
    {
        // This suffices to give actual path
        return p.streamReadContext().pathAsPointer().toString();
    }

    // [databind#2747]
    @Test
    void locationAccessWithExtTypeId() throws Exception {
        ObjectReader objectReader = newJsonMapper().readerFor(Wrapper.class);

        // Scalar value payload
        Wrapper wrapper = objectReader.readValue("{" +
                "\"type\":\"location\"," +
                "\"wrapped\": 1" +
                "}");
        assertEquals("/wrapped", ((Location) wrapper.wrapped).value);

        // Structured (Object) value payload: previously threw MismatchedInputException
        // "expected closing END_ARRAY" because of the synthetic WRAPPER_ARRAY.
        wrapper = objectReader.readValue("{" +
                "\"type\":\"location\"," +
                "\"wrapped\": {}" +
                "}");
        assertEquals("/wrapped", ((Location) wrapper.wrapped).value);

        // Type-id property appearing AFTER value: the value is buffered first and
        // replayed only once the type id is known. Without pinning the TokenBuffer's
        // parent context at buffering time, the outer parser's `currentName` has
        // advanced to "type" by replay time and leaks into the deserializer's view.
        wrapper = objectReader.readValue("{" +
                "\"wrapped\": 1," +
                "\"type\":\"location\"" +
                "}");
        assertEquals("/wrapped", ((Location) wrapper.wrapped).value);

        wrapper = objectReader.readValue("{" +
                "\"wrapped\": {}," +
                "\"type\":\"location\"" +
                "}");
        assertEquals("/wrapped", ((Location) wrapper.wrapped).value);
    }

    // [databind#2747]: creator path routes through `_deserialize()` rather than
    // `_deserializeAndSet()` ��� verify the fix applies there too.
    @Test
    void locationAccessWithExtTypeIdCreator() throws Exception {
        ObjectReader objectReader = newJsonMapper().readerFor(CreatorWrapper.class);

        CreatorWrapper w = objectReader.readValue("{" +
                "\"type\":\"location\"," +
                "\"wrapped\": 1" +
                "}");
        assertEquals("/wrapped", ((Location) w.wrapped).value);

        w = objectReader.readValue("{" +
                "\"wrapped\": {}," +
                "\"type\":\"location\"" +
                "}");
        assertEquals("/wrapped", ((Location) w.wrapped).value);
    }

    // [databind#2747]: defaultImpl path ��� type-id property missing in JSON, type id
    // is resolved via `extProp.getDefaultTypeId(ctxt)` and fed through the fast path.
    @Test
    void locationAccessWithExtTypeIdDefaultImpl() throws Exception {
        ObjectReader objectReader = newJsonMapper().readerFor(DefaultImplWrapper.class);

        DefaultImplWrapper w = objectReader.readValue("{\"wrapped\": {}}");
        assertEquals("/wrapped", ((Location) w.wrapped).value);
    }
}