ExternalPropertyWithJsonValueNull3547Test.java

package tools.jackson.databind.jsontype.ext;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonValue;

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

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

// [databind#3547]: When combining `As.EXTERNAL_PROPERTY` with `@JsonValue`, the serializer
// used to omit the external type id field when the `@JsonValue` return value was null.
public class ExternalPropertyWithJsonValueNull3547Test extends DatabindTestUtil
{
    public interface GenericType {
        String getValue();
        void setValue(String value);
    }

    public static abstract class AbstractGenericType implements GenericType {
        protected String value;
        public AbstractGenericType() { }
        public AbstractGenericType(String value) { this.value = value; }
        @Override @JsonValue public String getValue() { return value; }
        @Override public void setValue(String value) { this.value = value; }
    }

    public static class FooType extends AbstractGenericType {
        public FooType() { super(); }
        @JsonCreator public FooType(String value) { super(value); }
    }

    public static class Container {
        @JsonTypeInfo(use = Id.CLASS, include = As.EXTERNAL_PROPERTY, property = "type", visible = true)
        @JsonSubTypes(value = { @Type(value = FooType.class) })
        protected GenericType value;

        public GenericType getValue() { return value; }
        public void setValue(GenericType value) { this.value = value; }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void serializeWithNonNullJsonValue() throws Exception {
        Container container = new Container();
        container.setValue(new FooType("foobar"));
        String json = MAPPER.writeValueAsString(container);
        assertEquals(
            "{\"value\":\"foobar\",\"type\":\"" + FooType.class.getName() + "\"}",
            json);
    }

    // The type id must still be emitted even when the `@JsonValue` accessor returns null,
    // as long as the containing bean itself is non-null.
    @Test
    public void serializeWithNullJsonValueShouldStillEmitTypeId() throws Exception {
        Container container = new Container();
        container.setValue(new FooType()); // value bean present, but @JsonValue getter returns null
        String json = MAPPER.writeValueAsString(container);
        assertEquals(
            "{\"value\":null,\"type\":\"" + FooType.class.getName() + "\"}",
            json);
    }

    // Sanity check: if the bean itself is null, no type id should be emitted.
    @Test
    public void serializeWithNullBean() throws Exception {
        Container container = new Container();
        String json = MAPPER.writeValueAsString(container);
        assertEquals("{\"value\":null}", json);
    }

    @Test
    public void deserializeWithNonNullJsonValue() throws Exception {
        String json = "{\"value\":\"foobar\",\"type\":\"" + FooType.class.getName() + "\"}";
        Container container = MAPPER.readValue(json, Container.class);
        assertNotNull(container.value);
        assertInstanceOf(FooType.class, container.value);
        assertEquals("foobar", container.value.getValue());
    }

    @Test
    public void roundtripWithNonNullJsonValue() throws Exception {
        Container in = new Container();
        in.setValue(new FooType("foobar"));
        String json = MAPPER.writeValueAsString(in);
        Container out = MAPPER.readValue(json, Container.class);
        assertInstanceOf(FooType.class, out.value);
        assertEquals("foobar", out.value.getValue());
    }

    // 18-Apr-2026, tatu: Note: no deser test for surrogate `null` value as
    //   it is not quite clear what should happen (or rather, how to make
    //   round-trip handling work as expected)
}