ClassDeserNoStaticInitTest.java

package tools.jackson.databind.deser.jdk;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;

import tools.jackson.databind.ObjectMapper;

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

import static tools.jackson.databind.testutil.DatabindTestUtil.newJsonMapper;

// Resolving a `java.lang.Class` value from JSON must not force initialization
// (running the static initializer) of the named class.
public class ClassDeserNoStaticInitTest
{
    // Separate holder so that reading the flag does NOT initialize Gadget
    static class InitFlag {
        static volatile boolean gadgetInitialized = false;
    }

    static class Gadget {
        static {
            InitFlag.gadgetInitialized = true;
        }
    }

    static class Holder {
        public Class<?> type;
    }

    // Separate holder so that reading the flag does NOT initialize Sub
    static class SubInitFlag {
        static volatile boolean subInitialized = false;
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    static class Base { }

    static class Sub extends Base {
        static {
            SubInitFlag.subInitialized = true;
        }
        public int value;
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void classValueDoesNotTriggerStaticInitializer() throws Exception
    {
        assertFalse(InitFlag.gadgetInitialized);

        String json = "{\"type\":\"" + Gadget.class.getName() + "\"}";
        Holder h = MAPPER.readValue(json, Holder.class);

        // Class is still resolved correctly
        assertEquals(Gadget.class, h.type);
        // ...but its static initializer must not have run
        assertFalse(InitFlag.gadgetInitialized,
                "Deserializing a Class value must not run the target class static initializer");
    }

    // Counterpart invariant: when a polymorphic type id is not just resolved but
    // actually instantiated during deserialization, the JVM must (still) run the
    // subtype's static initializer -- resolving without init does not regress this.
    @Test
    public void polymorphicInstantiationTriggersStaticInitializer() throws Exception
    {
        // `Sub.class.getName()` (below) does not initialize Sub, so the flag
        // should still be clear before we deserialize an actual instance.
        assertFalse(SubInitFlag.subInitialized);

        String json = "{\"@class\":\"" + Sub.class.getName() + "\",\"value\":42}";
        Base result = MAPPER.readValue(json, Base.class);

        assertEquals(Sub.class, result.getClass());
        assertEquals(42, ((Sub) result).value);
        // Instantiating the subtype must have run its static initializer
        assertTrue(SubInitFlag.subInitialized,
                "Instantiating a polymorphic subtype must run its static initializer");
    }
}