ExternalTypeIdWithVisible1329Test.java

package tools.jackson.databind.jsontype.ext;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;

import tools.jackson.databind.*;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

/**
 * Test for [databind#1329]: External property deserialized even if visible = false
 */
public class ExternalTypeIdWithVisible1329Test extends DatabindTestUtil
{
    // Base types for polymorphic deserialization
    static abstract class InviteTo {
        public String name;
    }

    static class InviteToContact extends InviteTo {
        public InviteToContact() { }
        public InviteToContact(String n) { name = n; }
    }

    static class InviteToEmail extends InviteTo {
        public String email;
        public InviteToEmail() { }
        public InviteToEmail(String n, String e) {
            name = n;
            email = e;
        }
    }

    enum InviteKind {
        CONTACT, EMAIL
    }

    // Main class with external type id and visible=false
    // Issue: when visible=false, the external property field should NOT be populated
    static class Invite {
        public InviteKind kind;

        // This field is used as the external property for type information
        // With visible=false, it should NOT be populated during deserialization
        public InviteKind kindForMapper;

        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
                property = "kind",
                visible = false)
        @JsonSubTypes({
            @JsonSubTypes.Type(value = InviteToContact.class, name = "CONTACT"),
            @JsonSubTypes.Type(value = InviteToEmail.class, name = "EMAIL")
        })
        public InviteTo to;

        public Invite() { }
        public Invite(InviteKind k, InviteTo t) {
            kind = k;
            to = t;
        }

        @Override
        public String toString() {
            return "Invite(kind=" + kind + ", kindForMapper=" + kindForMapper + ", to=" + to + ")";
        }
    }

    // Alternative version where a separate field exists but should not be visible
    static class Invite2 {
        // This is the main field used for the type id (external property)
        public InviteKind kindForMapper;

        @JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
                include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
                property = "kindForMapper",
                visible = false)
        @JsonSubTypes({
            @JsonSubTypes.Type(value = InviteToContact.class, name = "CONTACT"),
            @JsonSubTypes.Type(value = InviteToEmail.class, name = "EMAIL")
        })
        public InviteTo to;

        public Invite2() { }
        public Invite2(InviteKind k, InviteTo t) {
            kindForMapper = k;
            to = t;
        }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void testExternalTypeIdNotVisibleInTypeInfo() throws Exception
    {
        // When visible=false in @JsonTypeInfo, the external type id property
        // should not be set on the enclosing object, even though "kind" exists in JSON
        String json = a2q("{'kind':'CONTACT','to':{'name':'Foo'}}");

        Invite result = MAPPER.readValue(json, Invite.class);

        assertNotNull(result);
        assertNotNull(result.to);
        assertInstanceOf(InviteToContact.class, result.to);
        assertEquals("Foo", result.to.name);

        // "kind" is both a regular bean property AND the external type id property name.
        // With visible=false, the type id value should be consumed for type resolution
        // and NOT set on the bean.
        assertNull(result.kind,
                "kind should be null when @JsonTypeInfo has visible=false");
        assertNull(result.kindForMapper,
                "kindForMapper should be null when it's not in JSON");
    }

    @Test
    public void testExternalTypeIdWithVisibleFalse() throws Exception
    {
        // When visible=false (the default), the external property used for type id
        // should not be populated in the target object during deserialization
        String json = a2q("{'kindForMapper':'EMAIL','to':{'name':'Bar','email':'test@example.com'}}");

        Invite2 result = MAPPER.readValue(json, Invite2.class);

        assertNotNull(result);
        assertNotNull(result.to);
        assertInstanceOf(InviteToEmail.class, result.to);
        assertEquals("Bar", result.to.name);
        assertEquals("test@example.com", ((InviteToEmail)result.to).email);

        // kindForMapper should remain null because visible=false means it
        // should only be used for type resolution, not populated in the object
        assertNull(result.kindForMapper,
                "kindForMapper should be null when @JsonTypeInfo has visible=false");
    }

    // Verify that EXTERNAL_TYPE_ID_ALWAYS_VISIBLE overrides visible=false
    @Test
    public void testExternalTypeIdAlwaysVisibleFeature() throws Exception
    {
        ObjectMapper mapper = jsonMapperBuilder()
                .enable(MapperFeature.EXTERNAL_TYPE_ID_ALWAYS_VISIBLE)
                .build();
        String json = a2q("{'kindForMapper':'EMAIL','to':{'name':'Bar','email':'test@example.com'}}");

        Invite2 result = mapper.readValue(json, Invite2.class);

        assertNotNull(result);
        assertNotNull(result.to);
        assertInstanceOf(InviteToEmail.class, result.to);
        // With EXTERNAL_TYPE_ID_ALWAYS_VISIBLE, the type property IS set even with visible=false
        assertEquals(InviteKind.EMAIL, result.kindForMapper);
    }
}