AnySetterForCreator562Test.java

package com.fasterxml.jackson.databind.deser.creators;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;

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

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;

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

// [databind#562] Allow @JsonAnySetter on Creator constructors
public class AnySetterForCreator562Test extends DatabindTestUtil
{
    static class POJO562
    {
        String a;
        Map<String,Object> stuff;

        @JsonCreator
        public POJO562(@JsonProperty("a") String a,
            @JsonAnySetter Map<String, Object> leftovers
        ) {
            this.a = a;
            stuff = leftovers;
        }
    }

    static class POJO562WithAnnotationOnBothCtorParamAndField
    {
        String a;
        @JsonAnySetter
        Map<String,Object> stuffFromField;
        Map<String,Object> stuffFromConstructor;

        @JsonCreator
        public POJO562WithAnnotationOnBothCtorParamAndField(@JsonProperty("a") String a,
                                                            @JsonAnySetter Map<String, Object> leftovers
        ) {
            this.a = a;
            stuffFromConstructor = leftovers;
        }
    }

    static class POJO562WithField
    {
        String a;
        Map<String,Object> stuff;

        public String b;

        @JsonCreator
        public POJO562WithField(@JsonProperty("a") String a,
            @JsonAnySetter Map<String, Object> leftovers
        ) {
            this.a = a;
            stuff = leftovers;
        }
    }

    static class PojoWithNodeAnySetter
    {
        String a;
        JsonNode anySetterNode;

        @JsonCreator
        public PojoWithNodeAnySetter(@JsonProperty("a") String a,
            @JsonAnySetter JsonNode leftovers
        ) {
            this.a = a;
            anySetterNode = leftovers;
        }
    }

    static class MultipleAny562
    {
        @JsonCreator
        public MultipleAny562(@JsonProperty("a") String a,
            @JsonAnySetter Map<String, Object> leftovers,
            @JsonAnySetter Map<String, Object> leftovers2) {
            throw new Error("Should never get here!");
        }
    }

    static class PojoWithDisabled
    {
        String a;
        Map<String,Object> stuff;

        @JsonCreator
        public PojoWithDisabled(@JsonProperty("a") String a,
            @JsonAnySetter(enabled = false) Map<String, Object> leftovers
        ) {
            this.a = a;
            stuff = leftovers;
        }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    @Test
    public void mapAnySetterViaCreator562() throws Exception
    {
        Map<String, Object> expected = new HashMap<>();
        expected.put("b", Integer.valueOf(42));
        expected.put("c", Integer.valueOf(111));

        POJO562 pojo = MAPPER.readValue(a2q(
                "{'a':'value', 'b':42, 'c': 111}"
                ),
                POJO562.class);

        assertEquals("value", pojo.a);
        assertEquals(expected, pojo.stuff);

        // Should be fine to vary ordering too
        pojo = MAPPER.readValue(a2q(
                "{'b':42, 'a':'value', 'c': 111}"
                ),
                POJO562.class);

        assertEquals("value", pojo.a);
        assertEquals(expected, pojo.stuff);

        // Should also initialize any-setter-Map even if no contents
        pojo = MAPPER.readValue(a2q("{'a':'value2'}"), POJO562.class);
        assertEquals("value2", pojo.a);
        assertEquals(new HashMap<>(), pojo.stuff);
    }

    // [databind#4634]
    @Test
    public void mapAnySetterViaCreatorWhenBothCreatorAndFieldAreAnnotated() throws Exception
    {
        Map<String, Object> expected = new HashMap<>();
        expected.put("b", Integer.valueOf(42));
        expected.put("c", Integer.valueOf(111));

        POJO562WithAnnotationOnBothCtorParamAndField pojo = MAPPER.readValue(a2q(
                "{'a':'value', 'b':42, 'c': 111}"
                ),
                POJO562WithAnnotationOnBothCtorParamAndField.class);

        assertEquals("value", pojo.a);
        assertEquals(expected, pojo.stuffFromConstructor);
        // In an ideal world, maybe exception should be thrown for annotating both field + constructor parameter,
        // but that scenario is possible in this imperfect world e.g. annotating `@JsonAnySetter` on a Record component
        // will cause that annotation to be (auto)propagated to both the field & constructor parameter (& accessor method)
        assertNull(pojo.stuffFromField);
    }

    // Creator and non-Creator props AND any-setter ought to be fine too
    @Test
    public void mapAnySetterViaCreatorAndField() throws Exception
    {
        POJO562WithField pojo = MAPPER.readValue(
                a2q("{'a':'value', 'b':'xyz', 'c': 'abc'}"),
                POJO562WithField.class);

        assertEquals("value", pojo.a);
        assertEquals("xyz", pojo.b);
        assertEquals(Collections.singletonMap("c", "abc"), pojo.stuff);
    }

    @Test
    public void testNodeAnySetterViaCreator562() throws Exception
    {
        PojoWithNodeAnySetter pojo = MAPPER.readValue(
                a2q("{'a':'value', 'b':42, 'c': 111}"),
                PojoWithNodeAnySetter.class);

        assertEquals("value", pojo.a);
        assertEquals(a2q("{'c':111,'b':42}"), pojo.anySetterNode + "");

        // Also ok to get nothing, resulting in empty ObjectNode
        pojo = MAPPER.readValue(a2q("{'a':'ok'}"), PojoWithNodeAnySetter.class);

        assertEquals("ok", pojo.a);
        assertEquals(MAPPER.createObjectNode(), pojo.anySetterNode);
    }

    @Test
    public void testAnyMapWithNullCreatorProp() throws Exception
    {
        ObjectMapper failOnNullMapper = jsonMapperBuilder()
            .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES).build();

        // 08-Jun-2024, tatu: Should be fine as we get empty Map for "no any setters"
        POJO562 value = failOnNullMapper.readValue(a2q("{'a':'value'}"), POJO562.class);
        assertEquals(Collections.emptyMap(), value.stuff);
    }

    @Test
    public void testAnyMapWithMissingCreatorProp() throws Exception
    {
        ObjectMapper failOnMissingMapper = jsonMapperBuilder()
            .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES).build();

        // Actually missing (no any props encountered)
        try {
            failOnMissingMapper.readValue(a2q("{'a':'value'}"), POJO562.class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Missing creator property ''");
        }

        // But should NOT fail if we did find at least one...
        POJO562 value = failOnMissingMapper.readValue(a2q("{'a':'value','b':'x'}"), POJO562.class);
        assertEquals(Collections.singletonMap("b", "x"), value.stuff);
    }

    @Test
    public void testAnyMapWithNullOrMissingCreatorProp() throws Exception
    {
        ObjectMapper failOnBothMapper = jsonMapperBuilder()
            .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
            .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)
            .build();
        try {
            failOnBothMapper.readValue(a2q("{'a':'value'}"), POJO562.class);
            fail("Should not pass");
        } catch (MismatchedInputException e) {
            verifyException(e, "Missing creator property ''");
        }
    }

    @Test
    public void testAnySetterViaCreator562FailForDup() throws Exception
    {
        try {
            MAPPER.readValue("{}", MultipleAny562.class);
            fail("Should not pass");
        } catch (InvalidDefinitionException e) {
            verifyException(e, "Invalid type definition");
            verifyException(e, "More than one 'any-setter'");
        }
    }

    @Test
    public void testAnySetterViaCreator562Disabled() throws Exception
    {
        try {
            MAPPER.readValue(a2q("{'a':'value', 'b':42, 'c': 111}"),
                PojoWithDisabled.class);
            fail("Should not pass");
        } catch (InvalidDefinitionException e) {
            verifyException(e, "Invalid type definition for type");
            verifyException(e, "has no property name (and is not Injectable): can not use as property-based Creator");
        }
    }
}