Skip to content

Java Record @JsonAnySetter value is null after deserialization #3439

@oujesky

Description

@oujesky

Describe the bug
When deserializing a Java Record with @JsonAnySetter annotated field the field is left as null and the unmapped values are ignored. Given the nature of Java records, there is no other way to get the unmapped fields values (like in case of annotating a setter method).

Version information
2.13.2.2

To Reproduce

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import java.util.Map;

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

public class JsonAnySetterRecordTest {

    record TestRecord(
        @JsonProperty String field,
        @JsonAnySetter Map<String, Object> anySetter
    ) {}

    @Test
    void testJsonAnySetterOnRecord() throws JsonProcessingException {
        var json = """
            {
                "field": "value",
                "unmapped1": "value1",
                "unmapped2": "value2"
            }
            """;

        var objectMapper = new ObjectMapper();

        var deserialized = objectMapper.readValue(json, TestRecord.class);

        assertEquals("value", deserialized.field());
        assertEquals(Map.of("unmapped1", "value1", "unmapped2", "value2"), deserialized.anySetter());
    }

}

Running this test the result will be:

Expected :{unmapped1=value1, unmapped2=value2}
Actual   :null

Expected behavior
The @JsonAnySetter annotated field should contain a Map instance with all unmapped fields and their values.

Additional context
The problem happens in com.fasterxml.jackson.databind.deser.SettableAnyProperty class in method set(...). The value of the @JsonAnySetter annotated field is null and therefore setting the property value is skipped.

The suggested solution would be for Java Record to provide a new empty Map instance for the annotated field to gather the unmapped properties and this would then be provided to Record's constructor when the deserialization is concluded. Given the immutable nature of Java Records, this should ideally be some kind of immutable Map implementation (i.e. Map.copyOf(...) ?)

There might be a workaround (which however won't be probably feasible in all cases) by supplying an additional @JsonCreator constructor or factory method, where the @JsonAnySetter field is initialized to an empty map and then to ensure the immutability, the getter for this map needs to be overridden and the value returned wrapped as immutable.

record TestRecord(
        String field,
        @JsonAnySetter Map<String, Object> anySetter
    ) {

        @JsonCreator
        TestRecord(@JsonProperty("field") String field) {
            this(field, new HashMap<>());
        }

        public Map<String, Object> anySetter() {
            return Collections.unmodifiableMap(anySetter);
        }
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    RecordIssue related to JDK17 java.lang.Record supporthas-failing-testIndicates that there exists a test case (under `failing/`) to reproduce the issue

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions