RecordBasicsTest.java

package com.fasterxml.jackson.databind.records;

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

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.Converter;

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

public class RecordBasicsTest extends DatabindTestUtil
{
    record EmptyRecord() { }

    record SimpleRecord(int id, String name) { }

    record RecordOfRecord(SimpleRecord record) { }

    record RecordWithRename(int id, @JsonProperty("rename")String name) { }

    record RecordWithHeaderInject(int id, @JacksonInject String name) { }

    record RecordWithConstructorInject(int id, String name) {

        RecordWithConstructorInject(int id, @JacksonInject String name) {
            this.id = id;
            this.name = name;
        }
    }

    // [databind#2992]
    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
    record SnakeRecord(String myId, String myValue){}

    record RecordWithJsonDeserialize(int id, @JsonDeserialize(converter = StringTrimmer.class) String name) { }

    record RecordSingleWriteOnly(@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) int id) { }

    record RecordSomeWriteOnly(
            @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) int id,
            @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String name,
            String email) {
    }

    record RecordAllWriteOnly(
            @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) int id,
            @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String name,
            @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) String email) {
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    /*
    /**********************************************************************
    /* Test methods, Record type introspection
    /**********************************************************************
     */

    @Test
    public void testClassUtil() {
        assertFalse(ClassUtil.isRecordType(getClass()));

        assertTrue(ClassUtil.isRecordType(SimpleRecord.class));
        assertTrue(ClassUtil.isRecordType(RecordOfRecord.class));
        assertTrue(ClassUtil.isRecordType(RecordWithRename.class));
    }

    @Test
    public void testRecordJavaType() {
        assertFalse(MAPPER.constructType(getClass()).isRecordType());

        assertTrue(MAPPER.constructType(SimpleRecord.class).isRecordType());
        assertTrue(MAPPER.constructType(RecordOfRecord.class).isRecordType());
        assertTrue(MAPPER.constructType(RecordWithRename.class).isRecordType());
    }

    /*
    /**********************************************************************
    /* Test methods, default reading/writing Record values
    /**********************************************************************
     */

    @Test
    public void testSerializeSimpleRecord() throws Exception {
        String json = MAPPER.writeValueAsString(new SimpleRecord(123, "Bob"));
        final Object EXP = map("id", Integer.valueOf(123), "name", "Bob");
        assertEquals(EXP, MAPPER.readValue(json, Object.class));
    }

    @Test
    public void testDeserializeSimpleRecord() throws Exception {
        assertEquals(new SimpleRecord(123, "Bob"),
                MAPPER.readValue("{\"id\":123,\"name\":\"Bob\"}", SimpleRecord.class));
    }

    @Test
    public void testSerializeEmptyRecord() throws Exception {
        assertEquals("{}", MAPPER.writeValueAsString(new EmptyRecord()));
    }

    @Test
    public void testDeserializeEmptyRecord() throws Exception {
        assertEquals(new EmptyRecord(),
                MAPPER.readValue("{}", EmptyRecord.class));
    }

    @Test
    public void testSerializeRecordOfRecord() throws Exception {
        RecordOfRecord record = new RecordOfRecord(new SimpleRecord(123, "Bob"));
        String json = MAPPER.writeValueAsString(record);
        final Object EXP = Collections.singletonMap("record",
                map("id", Integer.valueOf(123), "name", "Bob"));
        assertEquals(EXP, MAPPER.readValue(json, Object.class));
    }

    @Test
    public void testDeserializeRecordOfRecord() throws Exception {
        assertEquals(new RecordOfRecord(new SimpleRecord(123, "Bob")),
                MAPPER.readValue("{\"record\":{\"id\":123,\"name\":\"Bob\"}}",
                        RecordOfRecord.class));
    }

    /*
    /**********************************************************************
    /* Test methods, reading/writing Record values with different config
    /**********************************************************************
     */

    @Test
    public void testSerializeSimpleRecord_DisableAnnotationIntrospector() throws Exception {
        SimpleRecord record = new SimpleRecord(123, "Bob");

        JsonMapper mapper = JsonMapper.builder()
                .configure(MapperFeature.USE_ANNOTATIONS, false)
                .build();
        String json = mapper.writeValueAsString(record);

        assertEquals("{\"id\":123,\"name\":\"Bob\"}", json);
    }

    @Test
    public void testDeserializeSimpleRecord_DisableAnnotationIntrospector() throws Exception {
        JsonMapper mapper = JsonMapper.builder()
                .configure(MapperFeature.USE_ANNOTATIONS, false)
                .build();
        SimpleRecord value = mapper.readValue("{\"id\":123,\"name\":\"Bob\"}", SimpleRecord.class);

        assertEquals(new SimpleRecord(123, "Bob"), value);
    }

    /*
    /**********************************************************************
    /* Test methods, renames, injects
    /**********************************************************************
     */

    @Test
    public void testSerializeJsonRename() throws Exception {
        String json = MAPPER.writeValueAsString(new RecordWithRename(123, "Bob"));
        final Object EXP = map("id", Integer.valueOf(123), "rename", "Bob");
        assertEquals(EXP, MAPPER.readValue(json, Object.class));
    }

    @Test
    public void testDeserializeJsonRename() throws Exception {
        RecordWithRename value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}",
                RecordWithRename.class);
        assertEquals(new RecordWithRename(123, "Bob"), value);
    }

    /**
     * This test-case is just for documentation purpose:
     * GOTCHA: Annotations on header will be propagated to the field, leading to this failure.
     *
     * @see #testDeserializeConstructorInjectRecord()
     */
    @Test
    public void testDeserializeHeaderInjectRecord_WillFail() throws Exception {
        MAPPER.setInjectableValues(new InjectableValues.Std().addValue(String.class, "Bob"));

        try {
            MAPPER.readValue("{\"id\":123}", RecordWithHeaderInject.class);

            fail("should not pass");
        } catch (IllegalArgumentException e) {
            verifyException(e, "RecordWithHeaderInject#name");
            verifyException(e, "Can not set final java.lang.String field");
        }
    }

    @Test
    public void testDeserializeConstructorInjectRecord() throws Exception {
        MAPPER.setInjectableValues(new InjectableValues.Std().addValue(String.class, "Bob"));

        RecordWithConstructorInject value = MAPPER.readValue("{\"id\":123}", RecordWithConstructorInject.class);
        assertEquals(new RecordWithConstructorInject(123, "Bob"), value);
    }

    /*
    /**********************************************************************
    /* Test methods, naming strategy
    /**********************************************************************
     */

    // [databind#2992]
    @Test
    public void testNamingStrategy() throws Exception
    {
        SnakeRecord input = new SnakeRecord("123", "value");

        String json = MAPPER.writeValueAsString(input);
        assertEquals("{\"my_id\":\"123\",\"my_value\":\"value\"}", json);

        SnakeRecord output = MAPPER.readValue(json, SnakeRecord.class);
        assertEquals(input, output);
    }

    /*
    /**********************************************************************
    /* Test methods, JsonDeserialize
    /**********************************************************************
     */

    @Test
    public void testDeserializeJsonDeserializeRecord() throws Exception {
        RecordWithJsonDeserialize value = MAPPER.readValue("{\"id\":123,\"name\":\"   Bob   \"}", RecordWithJsonDeserialize.class);

        assertEquals(new RecordWithJsonDeserialize(123, "Bob"), value);
    }

    /*
    /**********************************************************************
    /* Test methods, JsonProperty(access=WRITE_ONLY)
    /**********************************************************************
     */

    @Test
    public void testSerialize_SingleWriteOnlyParameter() throws Exception {
        String json = MAPPER.writeValueAsString(new RecordSingleWriteOnly(123));

        assertEquals("{}", json);
    }

    // [databind#3897]
    @Test
    public void testDeserialize_SingleWriteOnlyParameter() throws Exception {
        RecordSingleWriteOnly value = MAPPER.readValue("{\"id\":123}", RecordSingleWriteOnly.class);

        assertEquals(new RecordSingleWriteOnly(123), value);
    }

    @Test
    public void testSerialize_SomeWriteOnlyParameter() throws Exception {
        String json = MAPPER.writeValueAsString(new RecordSomeWriteOnly(123, "Bob", "bob@example.com"));

        assertEquals("{\"email\":\"bob@example.com\"}", json);
    }

    @Test
    public void testDeserialize_SomeWriteOnlyParameter() throws Exception {
        RecordSomeWriteOnly value = MAPPER.readValue(
                "{\"id\":123,\"name\":\"Bob\",\"email\":\"bob@example.com\"}",
                RecordSomeWriteOnly.class);

        assertEquals(new RecordSomeWriteOnly(123, "Bob", "bob@example.com"), value);
    }

    @Test
    public void testSerialize_AllWriteOnlyParameter() throws Exception {
        String json = MAPPER.writeValueAsString(new RecordAllWriteOnly(123, "Bob", "bob@example.com"));

        assertEquals("{}", json);
    }

    @Test
    public void testDeserialize_AllWriteOnlyParameter() throws Exception {
        RecordAllWriteOnly value = MAPPER.readValue(
                "{\"id\":123,\"name\":\"Bob\",\"email\":\"bob@example.com\"}",
                RecordAllWriteOnly.class);

        assertEquals(new RecordAllWriteOnly(123, "Bob", "bob@example.com"), value);
    }

    /*
    /**********************************************************************
    /* Test method(s), MapperFeature.REQUIRE_SETTERS_FOR_GETTERS
    /**********************************************************************
     */

    // [databind#4678]
    @Test
    public void testSerializeWithSettersForGetters() throws Exception {
        ObjectMapper mapperWithSetters = JsonMapper.builder()
                .configure(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS, true)
                .build();
        var input = new SimpleRecord(123, "Bob");
        assertEquals(MAPPER.writeValueAsString(input),
                mapperWithSetters.writeValueAsString(input));
    }

    /*
    /**********************************************************************
    /* Internal helper methods
    /**********************************************************************
     */

    private Map<String,Object> map(String key1, Object value1,
            String key2, Object value2) {
        final Map<String, Object> result = new LinkedHashMap<>();
        result.put(key1, value1);
        result.put(key2, value2);
        return result;
    }

    public static class StringTrimmer implements Converter<String, String> {

        @Override
        public String convert(String value) {
            return value.trim();
        }

        @Override
        public JavaType getInputType(TypeFactory typeFactory) {
            return typeFactory.constructType(String.class);
        }

        @Override
        public JavaType getOutputType(TypeFactory typeFactory) {
            return typeFactory.constructType(String.class);
        }
    }
}