RecordImplicitCreatorsTest.java
package com.fasterxml.jackson.databind.records;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
public class RecordImplicitCreatorsTest extends DatabindTestUtil
{
record RecordWithImplicitFactoryMethods(BigDecimal id, String name) {
public static RecordWithImplicitFactoryMethods valueOf(int id) {
return new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(id), "IntFactoryMethod");
}
public static RecordWithImplicitFactoryMethods valueOf(double id) {
return new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(id), "DoubleFactoryMethod");
}
public static RecordWithImplicitFactoryMethods valueOf(String id) {
return new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(Double.parseDouble(id)), "StringFactoryMethod");
}
}
record RecordWithSingleValueConstructor(int id) {
}
record RecordWithSingleValueConstructorWithJsonValue(@JsonValue int id) {
}
record RecordWithSingleValueConstructorWithJsonValueAccessor(int id) {
@JsonValue
@Override
public int id() {
return id;
}
}
record RecordWithNonCanonicalConstructor(int id, String name, String email) {
public RecordWithNonCanonicalConstructor(int id, String email) {
this(id, "NonCanonicalConstructor", email);
}
}
/**
* Similar to:
* <pre>
* public class MyBean {
* ...
* // Single-arg constructor used by delegating creator.
* public MyBean(int id) { ... }
*
* // No-arg constructor used by properties-based creator.
* public MyBean() {}
*
* // Setters used by properties-based creator.
* public void setId(int id) { ... }
* public void setName(String name) { ... }
* }
* </pre>
*/
record RecordWithAltSingleValueConstructor(int id, String name) {
public RecordWithAltSingleValueConstructor(int id) {
this(id, "SingleValueConstructor");
}
}
private final ObjectMapper MAPPER = newJsonMapper();
/*
/**********************************************************************
/* Test methods, implicit factory methods & "implicit" canonical constructor
/**********************************************************************
*/
@Test
public void testDeserializeUsingImplicitIntegerFactoryMethod() throws Exception {
RecordWithImplicitFactoryMethods factoryMethodValue = MAPPER.readValue("123", RecordWithImplicitFactoryMethods.class);
assertEquals(new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(123), "IntFactoryMethod"), factoryMethodValue);
}
@Test
public void testDeserializeUsingImplicitDoubleFactoryMethod() throws Exception {
RecordWithImplicitFactoryMethods value = MAPPER.readValue("123.4", RecordWithImplicitFactoryMethods.class);
assertEquals(new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(123.4), "DoubleFactoryMethod"), value);
}
@Test
public void testDeserializeUsingImplicitStringFactoryMethod() throws Exception {
RecordWithImplicitFactoryMethods value = MAPPER.readValue("\"123.4\"", RecordWithImplicitFactoryMethods.class);
assertEquals(new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(123.4), "StringFactoryMethod"), value);
}
@Test
public void testDeserializeUsingImplicitCanonicalConstructor_WhenImplicitFactoryMethodsExist() throws Exception {
RecordWithImplicitFactoryMethods value = MAPPER.readValue(
"{\"id\":123.4,\"name\":\"CanonicalConstructor\"}",
RecordWithImplicitFactoryMethods.class);
assertEquals(new RecordWithImplicitFactoryMethods(BigDecimal.valueOf(123.4), "CanonicalConstructor"), value);
}
@Test
public void testDeserializeUsingImplicitFactoryMethod_WithAutoDetectCreatorsDisabled_WillFail() throws Exception {
ObjectMapper mapper = jsonMapperBuilder()
.disable(MapperFeature.AUTO_DETECT_CREATORS)
.build();
try {
mapper.readValue("123", RecordWithImplicitFactoryMethods.class);
fail("should not pass");
} catch (DatabindException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithImplicitFactoryMethod");
verifyException(e, "no int/Int-argument constructor/factory method");
}
}
/*
/**********************************************************************
/* Test methods, implicit single-value constructor
/**********************************************************************
*/
/**
* This test-case is just for documentation purpose:
* GOTCHA: For JavaBean, only having single-value constructor results in implicit delegating creator. But for
* Records, the CANONICAL single-value constructor results in properties-based creator.
* <p/>
* It will result in implicit delegating constructor only when:
* <ul>
* <li>
* There's NON-CANONICAL single-value constructor - see
* {@link #testDeserializeUsingImplicitDelegatingConstructor()}, or
* </li>
* <li>
* {@code @JsonValue} annotation is used - see
* {@link #testDeserializeUsingImplicitSingleValueConstructor_WithJsonValue()},
* {@link #testDeserializeUsingImplicitSingleValueConstructor_WithJsonValueAccessor()}
* </li>
* </ul>.
* <p/>
* yihtserns: maybe we can change this to adopt JavaBean's behaviour, but I prefer to not break existing behaviour
* until and unless there's a discussion on this.
*/
@Test
public void testDeserializeUsingImplicitSingleValueConstructor() throws Exception {
try {
// Cannot use delegating creator, unlike when dealing with JavaBean
MAPPER.readValue("123", RecordWithSingleValueConstructor.class);
fail("should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithSingleValueConstructor");
verifyException(e, "at least one Creator exists");
verifyException(e, "no int/Int-argument constructor/factory method");
}
// Can only use properties-based creator
RecordWithSingleValueConstructor value = MAPPER.readValue("{\"id\":123}", RecordWithSingleValueConstructor.class);
assertEquals(new RecordWithSingleValueConstructor(123), value);
}
/**
* This test-case is just for documentation purpose:
* See {@link #testDeserializeUsingImplicitSingleValueConstructor}
*/
@Test
public void testDeserializeSingleValueConstructor_WithDelegatingConstructorDetector_WillFail() throws Exception {
MAPPER.setConstructorDetector(ConstructorDetector.USE_DELEGATING);
try {
// Fail, no delegating creator
MAPPER.readValue("123", RecordWithSingleValueConstructor.class);
fail("should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithSingleValueConstructor");
verifyException(e, "at least one Creator exists");
verifyException(e, "no int/Int-argument constructor/factory method");
}
// Only have properties-based creator
RecordWithSingleValueConstructor value = MAPPER.readValue("{\"id\":123}", RecordWithSingleValueConstructor.class);
assertEquals(new RecordWithSingleValueConstructor(123), value);
}
/**
* This is just to catch any potential regression.
*/
@Test
public void testDeserializeSingleValueConstructor_WithPropertiesBasedConstructorDetector_WillFail() throws Exception {
MAPPER.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED);
try {
// This should fail
MAPPER.readValue("123", RecordWithSingleValueConstructor.class);
fail("should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithSingleValueConstructor");
verifyException(e, "at least one Creator exists");
verifyException(e, "no int/Int-argument constructor/factory method");
}
// This should work
RecordWithSingleValueConstructor value = MAPPER.readValue("{\"id\":123}", RecordWithSingleValueConstructor.class);
assertEquals(new RecordWithSingleValueConstructor(123), value);
}
/*
/**********************************************************************
/* Test methods, implicit single-value constructor + @JsonValue
/**********************************************************************
*/
/**
* [databind#3180]
* This test-case is just for documentation purpose:
* Unlike {@link #testDeserializeUsingImplicitSingleValueConstructor()}, annotating {@code @JsonValue}
* to a Record's header results in a delegating constructor.
*/
@Test
public void testDeserializeUsingImplicitSingleValueConstructor_WithJsonValue() throws Exception {
// Can use delegating creator
RecordWithSingleValueConstructorWithJsonValue value = MAPPER.readValue(
"123",
RecordWithSingleValueConstructorWithJsonValue.class);
assertEquals(new RecordWithSingleValueConstructorWithJsonValue(123), value);
try {
// Can no longer use properties-based creator
MAPPER.readValue("{\"id\":123}", RecordWithSingleValueConstructorWithJsonValue.class);
fail("should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithSingleValueConstructorWithJsonValue");
verifyException(e, "although at least one Creator exists");
verifyException(e, "cannot deserialize from Object value");
}
}
/**
* [databind#3180]
* This test-case is just for documentation purpose:
* Unlike {@link #testDeserializeUsingImplicitSingleValueConstructor()}, annotating {@code @JsonValue}
* to the accessor results in a delegating creator.
*/
@Test
public void testDeserializeUsingImplicitSingleValueConstructor_WithJsonValueAccessor() throws Exception {
// Can use delegating creator
RecordWithSingleValueConstructorWithJsonValueAccessor value = MAPPER.readValue(
"123",
RecordWithSingleValueConstructorWithJsonValueAccessor.class);
assertEquals(new RecordWithSingleValueConstructorWithJsonValueAccessor(123), value);
try {
// Can no longer use properties-based creator
MAPPER.readValue("{\"id\":123}", RecordWithSingleValueConstructorWithJsonValueAccessor.class);
fail("should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithSingleValueConstructorWithJsonValueAccessor");
verifyException(e, "although at least one Creator exists");
verifyException(e, "cannot deserialize from Object value");
}
}
/*
/**********************************************************************
/* Test methods, implicit properties-based + delegating constructor
/**********************************************************************
*/
@Test
public void testDeserializeUsingImplicitPropertiesBasedConstructor() throws Exception {
RecordWithAltSingleValueConstructor value = MAPPER.readValue(
"{\"id\":123,\"name\":\"PropertiesBasedConstructor\"}",
RecordWithAltSingleValueConstructor.class);
assertEquals(new RecordWithAltSingleValueConstructor(123, "PropertiesBasedConstructor"), value);
}
/**
* @see #testDeserializeUsingImplicitSingleValueConstructor()
*/
@Test
public void testDeserializeUsingImplicitDelegatingConstructor() throws Exception {
RecordWithAltSingleValueConstructor value = MAPPER.readValue("123", RecordWithAltSingleValueConstructor.class);
assertEquals(new RecordWithAltSingleValueConstructor(123, "SingleValueConstructor"), value);
}
/*
/**********************************************************************
/* Test methods, implicit parameter names
/**********************************************************************
*/
@Test
public void testDeserializeMultipleConstructorsRecord_WithImplicitParameterNames_WillUseCanonicalConstructor() throws Exception {
MAPPER.setAnnotationIntrospector(new Jdk8ConstructorParameterNameAnnotationIntrospector());
RecordWithNonCanonicalConstructor value = MAPPER.readValue(
"{\"id\":123,\"name\":\"Bob\",\"email\":\"bob@example.com\"}",
RecordWithNonCanonicalConstructor.class);
assertEquals(new RecordWithNonCanonicalConstructor(123, "Bob", "bob@example.com"), value);
}
@Test
public void testDeserializeMultipleConstructorsRecord_WithImplicitParameterNames_WillIgnoreNonCanonicalConstructor() throws Exception {
MAPPER.setAnnotationIntrospector(new Jdk8ConstructorParameterNameAnnotationIntrospector());
RecordWithNonCanonicalConstructor value = MAPPER.readValue(
"{\"id\":123,\"email\":\"bob@example.com\"}",
RecordWithNonCanonicalConstructor.class);
assertEquals(new RecordWithNonCanonicalConstructor(123, null, "bob@example.com"), value);
}
}