CreatorParamShadowedByReadOnly5975Test.java

package tools.jackson.databind.deser.creators;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

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

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

// [databind#5975]: A creator parameter whose implicit (parameter-name) name
// matches the explicit name of a sibling READ_ONLY accessor must still receive
// its value during deserialization; the READ_ONLY ignoral collected for the
// sibling must not shadow the creator parameter.
class CreatorParamShadowedByReadOnly5975Test extends DatabindTestUtil
{
    // READ_ONLY public field whose explicit name matches the creator param's
    // implicit name.
    static class FieldBean {
        @JsonProperty(value = "uri", access = JsonProperty.Access.READ_ONLY)
        public final String redactedUri;

        @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
        public FieldBean(String uri) {
            this.redactedUri = uri;
        }
    }

    // READ_ONLY getter (instead of public field) whose explicit name matches
    // the creator param's implicit name.
    static class GetterBean {
        private final String uri;

        @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
        public GetterBean(String uri) {
            this.uri = uri;
        }

        @JsonProperty(value = "uri", access = JsonProperty.Access.READ_ONLY)
        public String getRedactedUri() {
            return uri;
        }
    }

    // Original issue's shape: WRITE_ONLY annotation on the creator parameter
    // alongside the READ_ONLY field.
    static class WriteOnlyCtorBean {
        @JsonProperty(value = "uri", access = JsonProperty.Access.READ_ONLY)
        public final String redactedUri;

        @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
        public WriteOnlyCtorBean(@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String uri) {
            this.redactedUri = uri;
        }
    }

    // Round-trip: the READ_ONLY field is serialized (with redaction applied
    // by the constructor) and deserialization populates the creator parameter
    // from the same JSON name.
    static class RedactingRoundTripBean {
        @JsonProperty(value = "uri", access = JsonProperty.Access.READ_ONLY)
        public final String redactedUri;

        @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
        public RedactingRoundTripBean(String uri) {
            this.redactedUri = (uri == null) ? null : uri.replace("password", "***");
        }
    }

    private final ObjectMapper MAPPER = jsonMapperBuilder()
            .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .build();

    @Test
    public void testReadOnlyFieldDoesNotShadowCreator() throws Exception {
        FieldBean read = MAPPER.readValue("{\"uri\":\"foo\"}", FieldBean.class);
        assertEquals("foo", read.redactedUri);
    }

    @Test
    public void testReadOnlyGetterDoesNotShadowCreator() throws Exception {
        GetterBean read = MAPPER.readValue("{\"uri\":\"foo\"}", GetterBean.class);
        assertEquals("foo", read.uri);
    }

    @Test
    public void testWriteOnlyCtorParamWithReadOnlyField() throws Exception {
        WriteOnlyCtorBean read = MAPPER.readValue("{\"uri\":\"foo\"}", WriteOnlyCtorBean.class);
        assertEquals("foo", read.redactedUri);
    }

    @Test
    public void testRoundTripPreservesRedaction() throws Exception {
        RedactingRoundTripBean original = new RedactingRoundTripBean("my-password");
        String json = MAPPER.writeValueAsString(original);
        assertEquals("{\"uri\":\"my-***\"}", json);
        RedactingRoundTripBean parsed = MAPPER.readValue(json, RedactingRoundTripBean.class);
        assertEquals("my-***", parsed.redactedUri);
    }
}