RecordExplicitCreatorsTest.java
package com.fasterxml.jackson.databind.records;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
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.*;
public class RecordExplicitCreatorsTest extends DatabindTestUtil
{
record RecordWithOneJsonPropertyWithoutJsonCreator(int id, String name) {
public RecordWithOneJsonPropertyWithoutJsonCreator(@JsonProperty("id_only") int id) {
this(id, "JsonPropertyConstructor");
}
public static RecordWithOneJsonPropertyWithoutJsonCreator valueOf(int id) {
return new RecordWithOneJsonPropertyWithoutJsonCreator(id);
}
}
record RecordWithTwoJsonPropertyWithoutJsonCreator(int id, String name, String email) {
public RecordWithTwoJsonPropertyWithoutJsonCreator(@JsonProperty("the_id") int id, @JsonProperty("the_email") String email) {
this(id, "TwoJsonPropertyConstructor", email);
}
public static RecordWithTwoJsonPropertyWithoutJsonCreator valueOf(int id) {
return new RecordWithTwoJsonPropertyWithoutJsonCreator(id, "factory@example.com");
}
}
record RecordWithJsonPropertyAndImplicitPropertyWithoutJsonCreator(int id, String name, String email) {
public RecordWithJsonPropertyAndImplicitPropertyWithoutJsonCreator(@JsonProperty("id_only") int id, String email) {
this(id, "JsonPropertyConstructor", email);
}
}
record RecordWithJsonPropertyWithJsonCreator(int id, String name) {
@JsonCreator
public RecordWithJsonPropertyWithJsonCreator(@JsonProperty("id_only") int id) {
this(id, "JsonCreatorConstructor");
}
public static RecordWithJsonPropertyWithJsonCreator valueOf(int id) {
return new RecordWithJsonPropertyWithJsonCreator(id);
}
}
record RecordWithJsonPropertyAndImplicitPropertyWithJsonCreator(int id, String name, String email) {
@JsonCreator
public RecordWithJsonPropertyAndImplicitPropertyWithJsonCreator(@JsonProperty("id_only") int id, String email) {
this(id, "JsonPropertyConstructor", email);
}
}
record RecordWithMultiExplicitDelegatingConstructor(int id, String name) {
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public RecordWithMultiExplicitDelegatingConstructor(int id) {
this(id, "IntConstructor");
}
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public RecordWithMultiExplicitDelegatingConstructor(String id) {
this(Integer.parseInt(id), "StringConstructor");
}
}
record RecordWithDisabledJsonCreator(int id, String name) {
@JsonCreator(mode = JsonCreator.Mode.DISABLED)
RecordWithDisabledJsonCreator {
}
}
record RecordWithExplicitFactoryMethod(BigDecimal id, String name) {
@JsonCreator
public static RecordWithExplicitFactoryMethod valueOf(int id) {
return new RecordWithExplicitFactoryMethod(BigDecimal.valueOf(id), "IntFactoryMethod");
}
public static RecordWithExplicitFactoryMethod valueOf(double id) {
return new RecordWithExplicitFactoryMethod(BigDecimal.valueOf(id), "DoubleFactoryMethod");
}
@JsonCreator
public static RecordWithExplicitFactoryMethod valueOf(String id) {
return new RecordWithExplicitFactoryMethod(BigDecimal.valueOf(Double.parseDouble(id)), "StringFactoryMethod");
}
}
private final ObjectMapper MAPPER = jsonMapperBuilder()
.disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS) // So that test cases don't have to assert weird error messages
.build();
/*
/**********************************************************************
/* Test methods, "implicit" (?) constructor with JsonProperty without JsonCreator
/**********************************************************************
*/
@Test
public void testDeserializeUsingJsonPropertyConstructor_WithoutJsonCreator() throws Exception {
RecordWithOneJsonPropertyWithoutJsonCreator oneJsonPropertyValue = MAPPER.readValue(
"{\"id_only\":123}",
RecordWithOneJsonPropertyWithoutJsonCreator.class);
assertEquals(new RecordWithOneJsonPropertyWithoutJsonCreator(123), oneJsonPropertyValue);
RecordWithTwoJsonPropertyWithoutJsonCreator twoJsonPropertyValue = MAPPER.readValue(
"{\"the_id\":123,\"the_email\":\"bob@example.com\"}",
RecordWithTwoJsonPropertyWithoutJsonCreator.class);
assertEquals(new RecordWithTwoJsonPropertyWithoutJsonCreator(123, "bob@example.com"), twoJsonPropertyValue);
}
/**
* Only 1 properties-based creator allowed, so can no longer use the (un-annotated) canonical constructor.
*/
@Test
public void testDeserializeUsingCanonicalConstructor_WhenJsonPropertyConstructorExists_WillFail() throws Exception {
try {
MAPPER.readValue(
"{\"id\":123,\"name\":\"Bobby\"}",
RecordWithOneJsonPropertyWithoutJsonCreator.class);
fail("should not pass");
} catch (JsonMappingException e) {
verifyException(e, "Unrecognized field \"id\"");
verifyException(e, "RecordWithOneJsonPropertyWithoutJsonCreator");
verifyException(e, "one known property: \"id_only\"");
}
try {
MAPPER.readValue(
"{\"id\":123,\"name\":\"Bobby\",\"email\":\"bobby@example.com\"}",
RecordWithTwoJsonPropertyWithoutJsonCreator.class);
fail("should not pass");
} catch (JsonMappingException e) {
verifyException(e, "Unrecognized field \"id\"");
verifyException(e, "RecordWithTwoJsonPropertyWithoutJsonCreator");
verifyException(e, "2 known properties: \"the_id\", \"the_email\"");
}
}
@Test
public void testDeserializeUsingImplicitFactoryMethod_WhenJsonPropertyConstructorExists() throws Exception {
RecordWithOneJsonPropertyWithoutJsonCreator oneJsonPropertyValue = MAPPER.readValue(
"123",
RecordWithOneJsonPropertyWithoutJsonCreator.class);
assertEquals(RecordWithOneJsonPropertyWithoutJsonCreator.valueOf(123), oneJsonPropertyValue);
RecordWithTwoJsonPropertyWithoutJsonCreator twoJsonPropertyValue = MAPPER.readValue(
"123",
RecordWithTwoJsonPropertyWithoutJsonCreator.class);
assertEquals(RecordWithTwoJsonPropertyWithoutJsonCreator.valueOf(123), twoJsonPropertyValue);
}
/*
/**********************************************************************
/* Test methods, explicit constructor with JsonProperty with JsonCreator
/**********************************************************************
*/
@Test
public void testDeserializeUsingJsonCreatorConstructor() throws Exception {
RecordWithJsonPropertyWithJsonCreator value = MAPPER.readValue("{\"id_only\":123}", RecordWithJsonPropertyWithJsonCreator.class);
assertEquals(new RecordWithJsonPropertyWithJsonCreator(123), value);
}
/**
* Only 1 properties-based creator allowed, so can no longer use the (un-annotated) canonical constructor
*/
@Test
public void testDeserializeUsingCanonicalConstructor_WhenJsonCreatorConstructorExists_WillFail() throws Exception {
try {
MAPPER.readValue("{\"id\":123,\"name\":\"Bobby\"}", RecordWithJsonPropertyWithJsonCreator.class);
fail("should not pass");
} catch (JsonMappingException e) {
verifyException(e, "Unrecognized field \"id\"");
verifyException(e, "RecordWithJsonPropertyWithJsonCreator");
verifyException(e, "one known property: \"id_only\"");
}
}
// 23-May-2024, tatu: Logic changed as part of [databind#4515]: explicit properties-based
// Creator does NOT block implicit delegating Creators. So formerly (pre-2.18) failing
// case is now expected to pass.
@Test
public void testDeserializeUsingImplicitFactoryMethod_WhenJsonCreatorConstructorExists_WillFail() throws Exception {
RecordWithJsonPropertyWithJsonCreator value = MAPPER.readValue("123",
RecordWithJsonPropertyWithJsonCreator.class);
assertEquals(123, value.id());
assertEquals("JsonCreatorConstructor", value.name());
}
/*
/**********************************************************************
/* Test methods, multiple explicit delegating constructors
/**********************************************************************
*/
@Test
public void testDeserializeUsingExplicitDelegatingConstructors() throws Exception {
RecordWithMultiExplicitDelegatingConstructor intConstructorValue = MAPPER.readValue("123", RecordWithMultiExplicitDelegatingConstructor.class);
assertEquals(new RecordWithMultiExplicitDelegatingConstructor(123, "IntConstructor"), intConstructorValue);
RecordWithMultiExplicitDelegatingConstructor stringConstructorValue = MAPPER.readValue("\"123\"", RecordWithMultiExplicitDelegatingConstructor.class);
assertEquals(new RecordWithMultiExplicitDelegatingConstructor(123, "StringConstructor"), stringConstructorValue);
}
/*
/**********************************************************************
/* Test methods, JsonCreator.mode=DISABLED
/**********************************************************************
*/
@Test
public void testDeserializeUsingDisabledConstructors_WillFail() throws Exception {
try {
MAPPER.readValue("{\"id\":123,\"name\":\"Bobby\"}",
RecordWithDisabledJsonCreator.class);
fail("should not pass");
} catch (InvalidDefinitionException e) {
verifyException(e, "RecordWithDisabledJsonCreator");
verifyException(e, "Cannot construct instance");
verifyException(e, "no Creators, like default constructor, exist");
verifyException(e, "cannot deserialize from Object value");
}
}
/*
/**********************************************************************
/* Test methods, explicit factory methods
/**********************************************************************
*/
@Test
public void testDeserializeUsingExplicitFactoryMethods() throws Exception {
RecordWithExplicitFactoryMethod intFactoryValue = MAPPER.readValue("123", RecordWithExplicitFactoryMethod.class);
assertEquals(new RecordWithExplicitFactoryMethod(BigDecimal.valueOf(123), "IntFactoryMethod"), intFactoryValue);
RecordWithExplicitFactoryMethod stringFactoryValue = MAPPER.readValue("\"123.4\"", RecordWithExplicitFactoryMethod.class);
assertEquals(new RecordWithExplicitFactoryMethod(BigDecimal.valueOf(123.4), "StringFactoryMethod"), stringFactoryValue);
}
/**
* Implicit factory methods detection is only activated when there's no explicit (i.e. annotated
* with {@link JsonCreator}) factory methods.
*/
@Test
public void testDeserializeUsingImplicitFactoryMethods_WhenExplicitFactoryMethodsExist_WillFail() throws Exception {
try {
MAPPER.readValue("123.4", RecordWithExplicitFactoryMethod.class);
fail("should not pass");
} catch (MismatchedInputException e) {
verifyException(e, "Cannot construct instance");
verifyException(e, "RecordWithExplicitFactoryMethod");
verifyException(e, "although at least one Creator exists");
verifyException(e, "no double/Double-argument constructor/factory");
}
}
/**
* Just like how no-arg constructor + setters will still be used to deserialize JSON Object even when
* there's JsonCreator factory method(s) in the JavaBean class.
*/
@Test
public void testDeserializeUsingImplicitCanonicalConstructor_WhenFactoryMethodsExist() throws Exception {
RecordWithExplicitFactoryMethod value = MAPPER.readValue("{\"id\":123.4,\"name\":\"CanonicalConstructor\"}", RecordWithExplicitFactoryMethod.class);
assertEquals(new RecordWithExplicitFactoryMethod(BigDecimal.valueOf(123.4), "CanonicalConstructor"), value);
}
/*
/**********************************************************************
/* Test methods, implicit parameter names
/**********************************************************************
*/
@Test
public void testDeserializeMultipleConstructorsRecord_WithExplicitAndImplicitParameterNames_WithJsonCreator() throws Exception {
final ObjectMapper mapper = jsonMapperBuilder()
.disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
.annotationIntrospector(new Jdk8ConstructorParameterNameAnnotationIntrospector())
.build();
RecordWithJsonPropertyAndImplicitPropertyWithJsonCreator value = mapper.readValue(
"{\"id_only\":123,\"email\":\"bob@example.com\"}",
RecordWithJsonPropertyAndImplicitPropertyWithJsonCreator.class);
assertEquals(new RecordWithJsonPropertyAndImplicitPropertyWithJsonCreator(123, "bob@example.com"), value);
}
/**
* This test used to fail before 2.18; but with Bean Property introspection
* rewrite now works!
*
* @see #testDeserializeUsingJsonCreatorConstructor()
* @see #testDeserializeUsingCanonicalConstructor_WhenJsonCreatorConstructorExists_WillFail()
*/
@Test
public void testDeserializeMultipleConstructorsRecord_WithExplicitAndImplicitParameterNames() throws Exception {
final ObjectMapper mapper = jsonMapperBuilder()
.annotationIntrospector(new Jdk8ConstructorParameterNameAnnotationIntrospector())
.build();
RecordWithJsonPropertyAndImplicitPropertyWithoutJsonCreator value = mapper.readValue(
"{\"id_only\":123,\"email\":\"bob@example.com\"}",
RecordWithJsonPropertyAndImplicitPropertyWithoutJsonCreator.class);
assertEquals(123, value.id);
assertEquals("bob@example.com", value.email);
}
}