RecordUpdate3079Test.java

package tools.jackson.databind.records;

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

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import tools.jackson.databind.*;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

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

public class RecordUpdate3079Test extends DatabindTestUtil
{
    public record IdNameRecord(int id, String name) { }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record IgnoreAllRecord(int id) { }

    // Record with @JsonAnySetter that captures UPPER-CASE property names
    public record AnySetterRecord(int id, String name,
            @JsonAnySetter Map<String, Object> extra) { }

    static class IdNameWrapper {
        public IdNameRecord value;

        protected IdNameWrapper() { }
        public IdNameWrapper(IdNameRecord v) { value = v; }
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    // [databind#3079]: Should be able to update Record value directly
    @Test
    public void testDirectRecordUpdate() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = MAPPER.updateValue(orig,
                Collections.singletonMap("id", 137));
        assertNotNull(result);
        assertEquals(137, result.id());
        assertEquals("Bob", result.name());
        assertNotSame(orig, result);
    }

    // [databind#3079]: update with all properties overridden
    @Test
    public void testDirectRecordUpdateAllProperties() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = MAPPER.updateValue(orig,
                Collections.singletonMap("name", "Gary"));
        assertNotNull(result);
        assertNotSame(orig, result);
        assertEquals(123, result.id());
        assertEquals("Gary", result.name());
        assertNotSame(orig, result);
    }

    // [databind#3079]: update with no properties should return equivalent (but not same) Record
    @Test
    public void testDirectRecordUpdateNoProperties() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = MAPPER.updateValue(orig,
                Collections.emptyMap());
        assertNotNull(result);
        assertNotSame(orig, result);
        assertEquals(123, result.id());
        assertEquals("Bob", result.name());

        // Same with `null`:
        result = MAPPER.updateValue(orig, null);
        assertNotNull(result);
        // actually same instance, impl detail
        assertSame(orig, result);
    }

    // [databind#3079] also: should be able to update Record valued property
    @Test
    public void testRecordAsPropertyUpdate() throws Exception
    {
        IdNameRecord origRecord = new IdNameRecord(123, "Bob");
        IdNameWrapper orig = new IdNameWrapper(origRecord);

        IdNameWrapper delta = new IdNameWrapper(new IdNameRecord(200, "Gary"));
        IdNameWrapper result = MAPPER.updateValue(orig, delta);

        assertEquals(200, result.value.id());
        assertEquals("Gary", result.value.name());
        assertSame(orig, result);
        assertNotSame(origRecord, result.value);
    }

    // [databind#3079] exercise "ignore all" path
    @Test
    public void testIgnoreAllUnknown() throws Exception {
        IgnoreAllRecord orig = new IgnoreAllRecord(1);
        IgnoreAllRecord updated = MAPPER.updateValue(orig, Collections.singletonMap("value", 123));
        assertNotNull(updated);
        assertNotSame(orig, updated);
    }

    /*
    /**********************************************************************
    /* Tests via ObjectReader (readerForUpdating)
    /**********************************************************************
     */

    // [databind#3079]: update Record via ObjectReader, partial override
    @Test
    public void testReaderForUpdatingRecordPartial() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'id':137}"));
        assertNotNull(result);
        assertEquals(137, result.id());
        assertEquals("Bob", result.name());
        assertNotSame(orig, result);
    }

    // [databind#3079]: update Record via ObjectReader, all properties
    @Test
    public void testReaderForUpdatingRecordAllProps() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'id':456,'name':'Gary'}"));
        assertNotNull(result);
        assertEquals(456, result.id());
        assertEquals("Gary", result.name());
        assertNotSame(orig, result);
    }

    // [databind#3079]: update Record via ObjectReader, empty JSON object
    @Test
    public void testReaderForUpdatingRecordEmpty() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = MAPPER.readerForUpdating(orig)
                .readValue("{}");
        assertNotNull(result);
        // NOTE: will not be same instance in this particular case, but more of an impl detail
        assertEquals(123, result.id());
        assertEquals("Bob", result.name());

        // Similarly with `null`:
        result = MAPPER.readerForUpdating(orig).readValue("null");
        // NOTE: will be same instance in this particular case
        assertNotNull(result);
        assertSame(orig, result);
    }

    // [databind#3079]: update Record via ObjectReader, original unchanged
    @Test
    public void testReaderForUpdatingRecordOrigUnchanged() throws Exception
    {
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'id':999,'name':'Zed'}"));
        assertEquals(123, orig.id());
        assertEquals("Bob", orig.name());
    }

    // [databind#3079]: update Record-valued property via ObjectReader
    @Test
    public void testReaderForUpdatingRecordProperty() throws Exception
    {
        IdNameRecord origRecord = new IdNameRecord(123, "Bob");
        IdNameWrapper orig = new IdNameWrapper(origRecord);

        IdNameWrapper result = MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'value':{'id':200,'name':'Gary'}}"));
        assertEquals(200, result.value.id());
        assertEquals("Gary", result.value.name());
        assertSame(orig, result);
        assertNotSame(origRecord, result.value);
    }

    // [databind#3079]: unknown properties should be ignored when configured
    @Test
    public void testReaderForUpdatingRecordUnknownIgnored() throws Exception
    {
        ObjectMapper lenientMapper = JsonMapper.builder()
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .build();
        IdNameRecord orig = new IdNameRecord(123, "Bob");
        IdNameRecord result = lenientMapper.readerForUpdating(orig)
                .readValue(a2q("{'id':137,'unknown':'value'}"));
        assertNotNull(result);
        assertEquals(137, result.id());
        assertEquals("Bob", result.name());
    }

    // [databind#3079]: @JsonAnySetter captures UPPER-CASE property names
    //   that map to existing lower-case properties via any-setter Map
    @Test
    public void testReaderForUpdatingRecordWithAnySetter() throws Exception
    {
        AnySetterRecord orig = new AnySetterRecord(123, "Bob",
                Map.of("ID", 999, "NAME", "Old"));
        AnySetterRecord result = MAPPER.readerForUpdating(orig)
                .readValue(a2q("{'ID':456,'NAME':'Gary'}"));
        assertNotNull(result);
        // Regular properties should be pre-populated from original
        assertEquals(123, result.id());
        assertEquals("Bob", result.name());
        // UPPER-CASE properties should be captured by any-setter, overriding originals
        assertEquals(456, result.extra().get("ID"));
        assertEquals("Gary", result.extra().get("NAME"));
    }
}