From 9a42cca8f55e012c7f829a9d69bfb25937d42379 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sat, 24 May 2025 13:22:50 +0200 Subject: [PATCH 1/9] feat(#1381): useInput=TRUE in JacksonInject now make deserialization discard the injected value in favor of input (if any) --- .../deser/BeanDeserializerFactory.java | 19 ++- .../deser/inject/JacksonInject1381Test.java | 123 ++++++++++++++++++ .../JacksonInject1381WithOptionalTest.java | 115 ++++++++++++++++ .../inject}/JacksonInject2678Test.java | 6 +- 4 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java rename src/test/java/com/fasterxml/jackson/databind/{tofix => deser/inject}/JacksonInject2678Test.java (89%) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index c90bdef271..dd796121ec 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -817,11 +817,22 @@ protected void addInjectables(DeserializationContext ctxt, for (Map.Entry entry : raw.entrySet()) { AnnotatedMember m = entry.getValue(); final JacksonInject.Value injectableValue = introspector.findInjectableValue(m); - final Boolean optional = injectableValue == null ? null : injectableValue.getOptional(); + final Boolean optional; + final Boolean useInput; - builder.addInjectable(PropertyName.construct(m.getName()), - m.getType(), - beanDesc.getClassAnnotations(), m, entry.getKey(), optional); + if (injectableValue == null) { + optional = null; + useInput = null; + } else { + optional = injectableValue.getOptional(); + useInput = injectableValue.getUseInput(); + } + + if (!Boolean.TRUE.equals(useInput)) { + builder.addInjectable(PropertyName.construct(m.getName()), + m.getType(), + beanDesc.getClassAnnotations(), m, entry.getKey(), optional); + } } } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java new file mode 100644 index 0000000000..057f141a77 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java @@ -0,0 +1,123 @@ +package com.fasterxml.jackson.databind.deser.inject; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JacksonInject1381Test extends DatabindTestUtil { + + private static class InputDefault { + + @JacksonInject(value = "key") + private final String field; + + @JsonCreator + public InputDefault(@JsonProperty("field") final String field) { + this.field = field; + } + + public String getField() { + return field; + } + } + + private static class InputTrue { + + @JacksonInject(value = "key", useInput = OptBoolean.TRUE) + private final String field; + + @JsonCreator + public InputTrue(@JsonProperty("field") final String field) { + this.field = field; + } + + public String getField() { + return field; + } + } + + private static class InputFalse { + + @JacksonInject(value = "key", useInput = OptBoolean.FALSE) + private final String field; + + @JsonCreator + public InputFalse(@JsonProperty("field") final String field) { + this.field = field; + } + + public String getField() { + return field; + } + } + + private final String empty = "{}"; + private final String input = "{\"field\": \"input\"}"; + + private final ObjectMapper plainMapper = JsonMapper.builder().build(); + private final ObjectMapper injectedMapper = JsonMapper.builder() + .injectableValues(new InjectableValues.Std().addValue("key", "injected")) + .build(); + + @Test + @DisplayName("input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") + void test1() { + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefault.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrue.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalse.class)); + } + + @Test + @DisplayName("input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") + void test2() throws JsonProcessingException { + assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); + } + + @Test + @DisplayName("input YES, injectable NO, useInput DEFAULT|FALSE => exception") + void test3() { + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputDefault.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputFalse.class)); + } + + @Test + @DisplayName("input YES, injectable NO, useInput TRUE => input") + void test4() throws JsonProcessingException { + assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + } + + @Test + @DisplayName("input YES, injectable YES, useInput DEFAULT|FALSE => injected") + void test5() throws JsonProcessingException { + assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); + } + + @Test + @DisplayName("input YES, injectable YES, useInput TRUE => input") + void test6() throws JsonProcessingException { + assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java new file mode 100644 index 0000000000..adde3ada7b --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java @@ -0,0 +1,115 @@ +package com.fasterxml.jackson.databind.deser.inject; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JacksonInject1381WithOptionalTest extends DatabindTestUtil { + + private static class InputDefault { + + @JacksonInject(value = "key", optional = OptBoolean.TRUE) + private final String field; + + @JsonCreator + public InputDefault(@JsonProperty("field") final String field) { + this.field = field; + } + + public String getField() { + return field; + } + } + + private static class InputTrue { + + @JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = OptBoolean.TRUE) + private final String field; + + @JsonCreator + public InputTrue(@JsonProperty("field") final String field) { + this.field = field; + } + + public String getField() { + return field; + } + } + + private static class InputFalse { + + @JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) + private final String field; + + @JsonCreator + public InputFalse(@JsonProperty("field") final String field) { + this.field = field; + } + + public String getField() { + return field; + } + } + + private final String empty = "{}"; + private final String input = "{\"field\": \"input\"}"; + + private final ObjectMapper plainMapper = JsonMapper.builder().build(); + private final ObjectMapper injectedMapper = JsonMapper.builder() + .injectableValues(new InjectableValues.Std().addValue("key", "injected")) + .build(); + + @Test + @DisplayName("optional YES, input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") + void test1() { + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefault.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrue.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalse.class)); + } + + @Test + @DisplayName("optional YES, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") + void test2() throws JsonProcessingException { + assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); + } + + @Test + @DisplayName("optional YES, input YES, injectable NO, useInput DEFAULT|TRUE|FALSE => input") + void test3() throws JsonProcessingException { + assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + } + + @Test + @DisplayName("optional YES, input YES, injectable YES, useInput DEFAULT|FALSE => injected") + void test4() throws JsonProcessingException { + assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); + } + + @Test + @DisplayName("optional YES, input YES, injectable YES, useInput TRUE => input") + void test5() throws JsonProcessingException { + assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java similarity index 89% rename from src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java rename to src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java index 20f1d41abc..6de48d1ed0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.tofix; +package com.fasterxml.jackson.databind.deser.inject; import java.util.Objects; @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; -import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,7 +40,6 @@ public String getField2() { } // [databind#2678] - @JacksonTestFailureExpected @Test void readValueInjectables() throws Exception { final InjectableValues injectableValues = @@ -57,8 +55,6 @@ void readValueInjectables() throws Exception { final Some actualValuePresent = mapper.readValue( "{\"field1\": \"field1value\", \"field2\": \"field2value\"}", Some.class); assertEquals("field1value", actualValuePresent.getField1()); - - // if I comment @JacksonInject that is next to the property the valid assert is the correct one: assertEquals("field2value", actualValuePresent.getField2()); } } From d6bee5ff8b2609bea435cd0329c1c94b68bf9e34 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 4 Jun 2025 20:51:29 -0700 Subject: [PATCH 2/9] Change exception throws --- .../databind/deser/BeanDeserializerFactory.java | 1 + .../deser/inject/JacksonInject1381Test.java | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index dd796121ec..2102e086e7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -828,6 +828,7 @@ protected void addInjectables(DeserializationContext ctxt, useInput = injectableValue.getUseInput(); } + // 04-Jun-2025, tatu: [databind#1381]: default for "useInput" is false if (!Boolean.TRUE.equals(useInput)) { builder.addInjectable(PropertyName.construct(m.getName()), m.getType(), diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java index 057f141a77..24c56c0c97 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java @@ -1,17 +1,18 @@ package com.fasterxml.jackson.databind.deser.inject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.OptBoolean; -import com.fasterxml.jackson.core.JsonProcessingException; + import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -86,7 +87,7 @@ void test1() { @Test @DisplayName("input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") - void test2() throws JsonProcessingException { + void test2() throws Exception { assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); @@ -104,20 +105,20 @@ void test3() { @Test @DisplayName("input YES, injectable NO, useInput TRUE => input") - void test4() throws JsonProcessingException { + void test4() throws Exception { assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); } @Test @DisplayName("input YES, injectable YES, useInput DEFAULT|FALSE => injected") - void test5() throws JsonProcessingException { + void test5() throws Exception { assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); } @Test @DisplayName("input YES, injectable YES, useInput TRUE => input") - void test6() throws JsonProcessingException { + void test6() throws Exception { assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); } } From bc094d82ce9612c0f0e13ff9f554b27ba48a88ed Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 4 Jun 2025 21:00:43 -0700 Subject: [PATCH 3/9] ... --- .../deser/inject/JacksonInject1381Test.java | 20 +++++++++---------- .../deser/inject/JacksonInject3072Test.java | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java index 24c56c0c97..cc6f68d1c0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java @@ -17,10 +17,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -class JacksonInject1381Test extends DatabindTestUtil { - - private static class InputDefault { - +class JacksonInject1381Test extends DatabindTestUtil +{ + static class InputDefault + { @JacksonInject(value = "key") private final String field; @@ -34,8 +34,8 @@ public String getField() { } } - private static class InputTrue { - + static class InputTrue + { @JacksonInject(value = "key", useInput = OptBoolean.TRUE) private final String field; @@ -49,8 +49,8 @@ public String getField() { } } - private static class InputFalse { - + static class InputFalse + { @JacksonInject(value = "key", useInput = OptBoolean.FALSE) private final String field; @@ -67,8 +67,8 @@ public String getField() { private final String empty = "{}"; private final String input = "{\"field\": \"input\"}"; - private final ObjectMapper plainMapper = JsonMapper.builder().build(); - private final ObjectMapper injectedMapper = JsonMapper.builder() + private final ObjectMapper plainMapper = newJsonMapper(); + private final ObjectMapper injectedMapper = jsonMapperBuilder() .injectableValues(new InjectableValues.Std().addValue("key", "injected")) .build(); diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java index 904fd5983a..0264d97a9d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.annotation.OptBoolean; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; From a12a3d777caf31148bae6b7173c945577b7c3155 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 4 Jun 2025 21:14:57 -0700 Subject: [PATCH 4/9] Add release notes; broke tests by changing annotation placements --- release-notes/CREDITS-2.x | 11 +++++++ release-notes/VERSION-2.x | 6 ++++ .../deser/inject/JacksonInject1381Test.java | 31 +++++++++---------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index ae59365989..b0cc5200e7 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1945,6 +1945,17 @@ Ryan Schmitt (@rschmitt) (2.19.0) Giulio Longfils (@giulong) + * Contributed #1381: Add a way to specify "inject-only" with `@JacksonInject` + (2.20.0) + * Contributed fix for #2678: `@JacksonInject` added to property overrides value + from the JSON even if `useInput` is `OptBoolean.TRUE` + (2.20.0) * Contributed #3072: Allow specifying `@JacksonInject` does not fail when there's no corresponding value (2.20.0) + +Plamen Tanov (@ptanov) + * Reported #2678: `@JacksonInject` added to property overrides value from the JSON even if + `useInput` is `OptBoolean.TRUE` + (2.20.0) + diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 8d12168fe4..7c882ee45c 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,12 @@ Project: jackson-databind 2.20.0 (not yet released) +#1381: Add a way to specify "inject-only" with `@JacksonInject` + (contributed by Giulio L) +#2678: `@JacksonInject` added to property overrides value from the JSON even if + `useInput` is `OptBoolean.TRUE` + (reported by Plamen T) + (fix contributed by Giulio L) #3072: Allow specifying `@JacksonInject` does not fail when there's no corresponding value (requested by Lavender S) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java index cc6f68d1c0..e38c94828d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java @@ -11,7 +11,6 @@ import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; -import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -21,46 +20,46 @@ class JacksonInject1381Test extends DatabindTestUtil { static class InputDefault { - @JacksonInject(value = "key") - private final String field; + private final String _field; @JsonCreator - public InputDefault(@JsonProperty("field") final String field) { - this.field = field; + public InputDefault(@JacksonInject(value = "key") + @JsonProperty("field") final String field) { + _field = field; } public String getField() { - return field; + return _field; } } static class InputTrue { - @JacksonInject(value = "key", useInput = OptBoolean.TRUE) - private final String field; + private final String _field; @JsonCreator - public InputTrue(@JsonProperty("field") final String field) { - this.field = field; + public InputTrue(@JacksonInject(value = "key", useInput = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; } public String getField() { - return field; + return _field; } } static class InputFalse { - @JacksonInject(value = "key", useInput = OptBoolean.FALSE) - private final String field; + private final String _field; @JsonCreator - public InputFalse(@JsonProperty("field") final String field) { - this.field = field; + public InputFalse(@JacksonInject(value = "key", useInput = OptBoolean.FALSE) + @JsonProperty("field") final String field) { + _field = field; } public String getField() { - return field; + return _field; } } From 69c603ef1c2f8950ba11916eb72bb7d5cd77c21f Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sat, 7 Jun 2025 17:44:52 +0200 Subject: [PATCH 5/9] test: minor refactor to apply coding conventions --- .../JacksonInject1381WithOptionalTest.java | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java index adde3ada7b..12e7e11389 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java @@ -4,11 +4,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.OptBoolean; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; -import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,58 +14,61 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -class JacksonInject1381WithOptionalTest extends DatabindTestUtil { - - private static class InputDefault { - +class JacksonInject1381WithOptionalTest extends DatabindTestUtil +{ + static class InputDefault + { @JacksonInject(value = "key", optional = OptBoolean.TRUE) - private final String field; + @JsonProperty("field") + private final String _field; @JsonCreator public InputDefault(@JsonProperty("field") final String field) { - this.field = field; + _field = field; } public String getField() { - return field; + return _field; } } - private static class InputTrue { - + static class InputTrue + { @JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = OptBoolean.TRUE) - private final String field; + @JsonProperty("field") + private final String _field; @JsonCreator public InputTrue(@JsonProperty("field") final String field) { - this.field = field; + _field = field; } public String getField() { - return field; + return _field; } } - private static class InputFalse { - + static class InputFalse + { @JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) - private final String field; + @JsonProperty("field") + private final String _field; @JsonCreator public InputFalse(@JsonProperty("field") final String field) { - this.field = field; + _field = field; } public String getField() { - return field; + return _field; } } private final String empty = "{}"; private final String input = "{\"field\": \"input\"}"; - private final ObjectMapper plainMapper = JsonMapper.builder().build(); - private final ObjectMapper injectedMapper = JsonMapper.builder() + private final ObjectMapper plainMapper = newJsonMapper(); + private final ObjectMapper injectedMapper = jsonMapperBuilder() .injectableValues(new InjectableValues.Std().addValue("key", "injected")) .build(); @@ -86,7 +87,7 @@ void test1() { @Test @DisplayName("optional YES, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") - void test2() throws JsonProcessingException { + void test2() throws Exception { assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); @@ -94,7 +95,7 @@ void test2() throws JsonProcessingException { @Test @DisplayName("optional YES, input YES, injectable NO, useInput DEFAULT|TRUE|FALSE => input") - void test3() throws JsonProcessingException { + void test3() throws Exception { assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField()); assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField()); assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); @@ -102,14 +103,14 @@ void test3() throws JsonProcessingException { @Test @DisplayName("optional YES, input YES, injectable YES, useInput DEFAULT|FALSE => injected") - void test4() throws JsonProcessingException { + void test4() throws Exception { assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); } @Test @DisplayName("optional YES, input YES, injectable YES, useInput TRUE => input") - void test5() throws JsonProcessingException { + void test5() throws Exception { assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); } } From 191292c75e15d50c1a9df9b096613048a5535070 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sun, 15 Jun 2025 20:28:21 +0200 Subject: [PATCH 6/9] test(#1381): covering all cases for JacksonInject, whether we useInput, optional, and/or the DeserializationFeature --- ...381DeserializationFeatureDisabledTest.java | 178 +++++++++++++++++ .../deser/inject/JacksonInject1381Test.java | 78 +++++++- ...nalDeserializationFeatureDisabledTest.java | 180 ++++++++++++++++++ .../JacksonInject1381WithOptionalTest.java | 61 ++++++ 4 files changed, 491 insertions(+), 6 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java new file mode 100644 index 0000000000..48ccc42a32 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java @@ -0,0 +1,178 @@ +package com.fasterxml.jackson.databind.deser.inject; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JacksonInject1381DeserializationFeatureDisabledTest extends DatabindTestUtil { + static class InputDefault { + @JacksonInject(value = "key") + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputDefault(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputDefaultConstructor { + private final String _field; + + @JsonCreator + public InputDefaultConstructor(@JacksonInject(value = "key") + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrue { + @JacksonInject(value = "key", useInput = OptBoolean.TRUE) + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputTrue(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrueConstructor { + private final String _field; + + @JsonCreator + public InputTrueConstructor(@JacksonInject(value = "key", useInput = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + + } + + static class InputFalse { + @JacksonInject(value = "key", useInput = OptBoolean.FALSE) + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputFalse(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputFalseConstructor { + private final String _field; + + @JsonCreator + public InputFalseConstructor(@JacksonInject(value = "key", useInput = OptBoolean.FALSE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + private final String empty = "{}"; + private final String input = "{\"field\": \"input\"}"; + + private final ObjectMapper plainMapper = newJsonMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE); + private final ObjectMapper injectedMapper = jsonMapperBuilder() + .injectableValues(new InjectableValues.Std().addValue("key", "injected")) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) + .build(); + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") + void test1() { + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputDefault.class)); + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputDefaultConstructor.class)); + + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputTrue.class)); + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputTrueConstructor.class)); + + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputFalse.class)); + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputFalseConstructor.class)); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") + void test2() throws Exception { + assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputDefaultConstructor.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrueConstructor.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalseConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable NO, useInput DEFAULT|FALSE => exception") + void test3() throws Exception { + assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputDefaultConstructor.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputFalseConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable NO, useInput TRUE => input") + void test4() throws Exception { + assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable YES, useInput DEFAULT|FALSE => injected") + void test5() throws Exception { + assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputDefaultConstructor.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalseConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable YES, useInput TRUE => input") + void test6() throws Exception { + assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java index e38c94828d..1473d3516d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java @@ -20,11 +20,27 @@ class JacksonInject1381Test extends DatabindTestUtil { static class InputDefault { + @JacksonInject(value = "key") + @JsonProperty("field") private final String _field; @JsonCreator - public InputDefault(@JacksonInject(value = "key") - @JsonProperty("field") final String field) { + public InputDefault(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputDefaultConstructor + { + private final String _field; + + @JsonCreator + public InputDefaultConstructor(@JacksonInject(value = "key") + @JsonProperty("field") final String field) { _field = field; } @@ -34,27 +50,60 @@ public String getField() { } static class InputTrue + { + @JacksonInject(value = "key", useInput = OptBoolean.TRUE) + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputTrue(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrueConstructor { private final String _field; @JsonCreator - public InputTrue(@JacksonInject(value = "key", useInput = OptBoolean.TRUE) - @JsonProperty("field") final String field) { + public InputTrueConstructor(@JacksonInject(value = "key", useInput = OptBoolean.TRUE) + @JsonProperty("field") final String field) { _field = field; } public String getField() { return _field; } + } static class InputFalse { + @JacksonInject(value = "key", useInput = OptBoolean.FALSE) + @JsonProperty("field") private final String _field; @JsonCreator - public InputFalse(@JacksonInject(value = "key", useInput = OptBoolean.FALSE) - @JsonProperty("field") final String field) { + public InputFalse(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputFalseConstructor + { + private final String _field; + + @JsonCreator + public InputFalseConstructor(@JacksonInject(value = "key", useInput = OptBoolean.FALSE) + @JsonProperty("field") final String field) { _field = field; } @@ -76,20 +125,29 @@ public String getField() { void test1() { assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(empty, InputDefault.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefaultConstructor.class)); assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(empty, InputTrue.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrueConstructor.class)); assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(empty, InputFalse.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalseConstructor.class)); } @Test @DisplayName("input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") void test2() throws Exception { assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputDefaultConstructor.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrueConstructor.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalseConstructor.class).getField()); } @Test @@ -97,27 +155,35 @@ void test2() throws Exception { void test3() { assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(input, InputDefault.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputDefaultConstructor.class)); assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(input, InputFalse.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputFalseConstructor.class)); } @Test @DisplayName("input YES, injectable NO, useInput TRUE => input") void test4() throws Exception { assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); } @Test @DisplayName("input YES, injectable YES, useInput DEFAULT|FALSE => injected") void test5() throws Exception { assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputDefaultConstructor.class).getField()); assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalseConstructor.class).getField()); } @Test @DisplayName("input YES, injectable YES, useInput TRUE => input") void test6() throws Exception { assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java new file mode 100644 index 0000000000..0d173a8fde --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java @@ -0,0 +1,180 @@ +package com.fasterxml.jackson.databind.deser.inject; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.ValueInstantiationException; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JacksonInject1381WithOptionalDeserializationFeatureDisabledTest extends DatabindTestUtil +{ + static class InputDefault + { + @JacksonInject(value = "key", optional = OptBoolean.TRUE) + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputDefault(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputDefaultConstructor + { + private final String _field; + + @JsonCreator + public InputDefaultConstructor(@JacksonInject(value = "key", optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrue + { + @JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = OptBoolean.TRUE) + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputTrue(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrueConstructor + { + private final String _field; + + @JsonCreator + public InputTrueConstructor(@JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + + } + + static class InputFalse + { + @JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) + @JsonProperty("field") + private final String _field; + + @JsonCreator + public InputFalse(@JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputFalseConstructor + { + private final String _field; + + @JsonCreator + public InputFalseConstructor(@JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + private final String empty = "{}"; + private final String input = "{\"field\": \"input\"}"; + + private final ObjectMapper plainMapper = newJsonMapper() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE); + private final ObjectMapper injectedMapper = jsonMapperBuilder() + .injectableValues(new InjectableValues.Std().addValue("key", "injected")) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) + .build(); + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, optional YES, input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") + void test1() { + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputDefault.class)); + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputDefaultConstructor.class)); + + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputTrue.class)); + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputTrueConstructor.class)); + + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputFalse.class)); + assertThrows(ValueInstantiationException.class, + () -> plainMapper.readValue(empty, InputFalseConstructor.class)); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, optional YES, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") + void test2() throws Exception { + assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputDefaultConstructor.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrueConstructor.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalseConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, optional YES, input YES, injectable NO, useInput DEFAULT|TRUE|FALSE => input") + void test3() throws Exception { + assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputDefaultConstructor.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputFalseConstructor.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, optional YES, input YES, injectable YES, useInput DEFAULT|FALSE => injected") + void test4() throws Exception { + assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputDefaultConstructor.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalseConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, optional YES, input YES, injectable YES, useInput TRUE => input") + void test5() throws Exception { + assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java index 12e7e11389..990d6a501e 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java @@ -32,6 +32,21 @@ public String getField() { } } + static class InputDefaultConstructor + { + private final String _field; + + @JsonCreator + public InputDefaultConstructor(@JacksonInject(value = "key", optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + static class InputTrue { @JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = OptBoolean.TRUE) @@ -48,6 +63,22 @@ public String getField() { } } + static class InputTrueConstructor + { + private final String _field; + + @JsonCreator + public InputTrueConstructor(@JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + + } + static class InputFalse { @JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) @@ -64,6 +95,21 @@ public String getField() { } } + static class InputFalseConstructor + { + private final String _field; + + @JsonCreator + public InputFalseConstructor(@JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + private final String empty = "{}"; private final String input = "{\"field\": \"input\"}"; @@ -77,40 +123,55 @@ public String getField() { void test1() { assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(empty, InputDefault.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefaultConstructor.class)); assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(empty, InputTrue.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrueConstructor.class)); assertThrows(MissingInjectableValueExcepion.class, () -> plainMapper.readValue(empty, InputFalse.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalseConstructor.class)); } @Test @DisplayName("optional YES, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") void test2() throws Exception { assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputDefaultConstructor.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputTrueConstructor.class).getField()); assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(empty, InputFalseConstructor.class).getField()); } @Test @DisplayName("optional YES, input YES, injectable NO, useInput DEFAULT|TRUE|FALSE => input") void test3() throws Exception { assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputDefaultConstructor.class).getField()); assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputFalseConstructor.class).getField()); assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); } @Test @DisplayName("optional YES, input YES, injectable YES, useInput DEFAULT|FALSE => injected") void test4() throws Exception { assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputDefaultConstructor.class).getField()); assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); + assertEquals("injected", injectedMapper.readValue(input, InputFalseConstructor.class).getField()); } @Test @DisplayName("optional YES, input YES, injectable YES, useInput TRUE => input") void test5() throws Exception { assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); } } From 5705aff994264477cb7e1c44a98fa510f78bd6b9 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sun, 15 Jun 2025 20:29:50 +0200 Subject: [PATCH 7/9] feat(#1381): JacksonInject.useInput works for constructor properties as well --- .../databind/deser/CreatorProperty.java | 5 +++++ .../databind/deser/SettableBeanProperty.java | 9 +++++++++ .../deser/impl/PropertyValueBuffer.java | 20 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java index e6d7df4b18..c5a9df0395 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java @@ -280,6 +280,11 @@ public Object getInjectableValueId() { return (_injectableValue == null) ? null : _injectableValue.getId(); } + @Override + public JacksonInject.Value getInjectableValue() { + return _injectableValue; + } + @Override public boolean isInjectionOnly() { return (_injectableValue != null) && !_injectableValue.willUseInput(true); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java index 5c02e63bb2..baee3fce90 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.impl.FailingDeserializer; @@ -458,6 +459,14 @@ public int getCreatorIndex() { */ public Object getInjectableValueId() { return null; } + /** + * Accessor for injectable value, if this bean property supports + * value injection. + * + * @since 2.20 + */ + public JacksonInject.Value getInjectableValue() { return null; } + /** * Accessor for checking whether this property is injectable, and if so, * ONLY injectable (will not bind from input). diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index a3c4e81d39..0495f26e9a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.BitSet; +import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.SettableAnyProperty; @@ -218,6 +219,25 @@ public Object[] getParameters(SettableBeanProperty[] props) if (_anyParamSetter != null) { _creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue(); } + + for (int ix = 0; ix < props.length; ++ix) { + final SettableBeanProperty prop = props[ix]; + final AnnotatedMember member = prop.getMember(); + + if (member != null) { + final JacksonInject.Value injectableValue = prop.getInjectableValue(); + + if (injectableValue != null && !Boolean.TRUE.equals(injectableValue.getUseInput())) { + final Object injectedValue = _context.findInjectableValue( + injectableValue.getId(), prop, member, injectableValue.getOptional()); + + if (injectedValue != JacksonInject.Value.empty()) { + _creatorParameters[ix] = injectedValue; + } + } + } + } + if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { if (_creatorParameters[ix] == null) { From 5da46ac39daa63138b882449c1e22c09272d746d Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Fri, 20 Jun 2025 17:00:38 +0200 Subject: [PATCH 8/9] test(#1381): minor code cleaning removing unused getters --- .../databind/deser/inject/JacksonInject3072Test.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java index 0264d97a9d..a5d399de31 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java @@ -22,14 +22,6 @@ static class DtoWithOptional { @JacksonInject(value = "optionalField", optional = OptBoolean.TRUE) String optionalField; - - public String getId() { - return id; - } - - public String getOptionalField() { - return optionalField; - } } static class DtoWithRequired { From deb17416f4f3fa2c28e6814f9ecba9a0591f2fc9 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Fri, 20 Jun 2025 17:53:13 +0200 Subject: [PATCH 9/9] feat(#1381): always adding properties annotated with JacksonInject to the injectables --- .../databind/DeserializationContext.java | 13 ++++++---- .../jackson/databind/InjectableValues.java | 6 ++--- .../deser/BeanDeserializerBuilder.java | 6 ++--- .../deser/BeanDeserializerFactory.java | 20 ++++------------ .../deser/impl/PropertyValueBuffer.java | 18 +++++++------- .../databind/deser/impl/ValueInjector.java | 24 +++++++++++++++---- .../deser/std/StdValueInstantiator.java | 3 ++- .../introspect/IntrospectorPairTest.java | 2 +- .../databind/tofix/JacksonInject4218Test.java | 6 +++-- 9 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index b18244bec3..06037fa79e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -465,21 +465,24 @@ public final boolean hasSomeOfFeatures(int featureMask) { * @since 2.20 */ public final Object findInjectableValue(Object valueId, - BeanProperty forProperty, Object beanInstance, Boolean optional) + BeanProperty forProperty, Object beanInstance, Boolean optional, Boolean useInput) throws JsonMappingException { if (_injectableValues == null) { // `optional` comes from property annotation (if any); has precedence // over global setting. - if (Boolean.TRUE.equals(optional) - || (optional == null && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) { + if (Boolean.TRUE.equals(useInput) + || Boolean.TRUE.equals(optional) + || (useInput == null || optional == null) + && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE)) { return JacksonInject.Value.empty(); } throw missingInjectableValueException(String.format( "No 'injectableValues' configured, cannot inject value with id '%s'", valueId), valueId, forProperty, beanInstance); } - return _injectableValues.findInjectableValue(this, valueId, forProperty, beanInstance, optional); + return _injectableValues.findInjectableValue(this, valueId, forProperty, beanInstance, + optional, useInput); } /** @@ -490,7 +493,7 @@ public final Object findInjectableValue(Object valueId, BeanProperty forProperty, Object beanInstance) throws JsonMappingException { - return findInjectableValue(valueId, forProperty, beanInstance, null); + return findInjectableValue(valueId, forProperty, beanInstance, null, null); } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java b/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java index 9950570696..0536e29e31 100644 --- a/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java +++ b/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java @@ -28,7 +28,7 @@ public abstract class InjectableValues */ public Object findInjectableValue(DeserializationContext ctxt, Object valueId, BeanProperty forProperty, Object beanInstance, - Boolean optional) + Boolean optional, Boolean useInput) throws JsonMappingException { // For backwards-compatibility, must delegate to old method @@ -83,7 +83,7 @@ public Std addValue(Class classKey, Object value) { */ @Override public Object findInjectableValue(DeserializationContext ctxt, Object valueId, - BeanProperty forProperty, Object beanInstance, Boolean optional) + BeanProperty forProperty, Object beanInstance, Boolean optional, Boolean useInput) throws JsonMappingException { if (!(valueId instanceof String)) { @@ -116,7 +116,7 @@ public Object findInjectableValue(DeserializationContext ctxt, Object valueId, public Object findInjectableValue(Object valueId, DeserializationContext ctxt, BeanProperty forProperty, Object beanInstance) throws JsonMappingException { - return this.findInjectableValue(ctxt, valueId, forProperty, beanInstance, null); + return this.findInjectableValue(ctxt, valueId, forProperty, beanInstance, null, null); } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java index 6888cb57ec..5d935b3c0a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java @@ -244,7 +244,7 @@ public void addBackReferenceProperty(String referenceName, SettableBeanProperty */ public void addInjectable(PropertyName propName, JavaType propType, Annotations contextAnnotations, AnnotatedMember member, - Object valueId, Boolean optional) + Object valueId, Boolean optional, Boolean useInput) throws JsonMappingException { if (_injectables == null) { @@ -257,7 +257,7 @@ public void addInjectable(PropertyName propName, JavaType propType, _handleBadAccess(e); } } - _injectables.add(new ValueInjector(propName, propType, member, valueId, optional)); + _injectables.add(new ValueInjector(propName, propType, member, valueId, optional, useInput)); } /** @@ -269,7 +269,7 @@ public void addInjectable(PropertyName propName, JavaType propType, Object valueId) throws JsonMappingException { - this.addInjectable(propName, propType, contextAnnotations, member, valueId, null); + this.addInjectable(propName, propType, contextAnnotations, member, valueId, null, null); } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 597d340301..13865b754c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -822,23 +822,11 @@ protected void addInjectables(DeserializationContext ctxt, for (Map.Entry entry : raw.entrySet()) { AnnotatedMember m = entry.getValue(); final JacksonInject.Value injectableValue = introspector.findInjectableValue(m); - final Boolean optional; - final Boolean useInput; - if (injectableValue == null) { - optional = null; - useInput = null; - } else { - optional = injectableValue.getOptional(); - useInput = injectableValue.getUseInput(); - } - - // 04-Jun-2025, tatu: [databind#1381]: default for "useInput" is false - if (!Boolean.TRUE.equals(useInput)) { - builder.addInjectable(PropertyName.construct(m.getName()), - m.getType(), - beanDesc.getClassAnnotations(), m, entry.getKey(), optional); - } + builder.addInjectable(PropertyName.construct(m.getName()), + m.getType(), + beanDesc.getClassAnnotations(), m, entry.getKey(), + injectableValue.getOptional(), injectableValue.getUseInput()); } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 0495f26e9a..7d5d894f44 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -222,17 +222,17 @@ public Object[] getParameters(SettableBeanProperty[] props) for (int ix = 0; ix < props.length; ++ix) { final SettableBeanProperty prop = props[ix]; - final AnnotatedMember member = prop.getMember(); + final JacksonInject.Value injectableValue = prop.getInjectableValue(); - if (member != null) { - final JacksonInject.Value injectableValue = prop.getInjectableValue(); + if (injectableValue != null) { + final Boolean useInput = injectableValue.getUseInput(); - if (injectableValue != null && !Boolean.TRUE.equals(injectableValue.getUseInput())) { - final Object injectedValue = _context.findInjectableValue( - injectableValue.getId(), prop, member, injectableValue.getOptional()); + if (!Boolean.TRUE.equals(useInput)) { + final Object value = _context.findInjectableValue(injectableValue.getId(), + prop, prop.getMember(), injectableValue.getOptional(), useInput); - if (injectedValue != JacksonInject.Value.empty()) { - _creatorParameters[ix] = injectedValue; + if (value != JacksonInject.Value.empty()) { + _creatorParameters[ix] = value; } } } @@ -289,7 +289,7 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { return _context.findInjectableValue(prop.getInjectableValueId(), - prop, null, null); + prop, null, null, null); } // Second: required? if (prop.isRequired()) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java index c6c4b2aad0..d8c6d057c7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java @@ -29,15 +29,24 @@ public class ValueInjector */ protected final Boolean _optional; + /** + * Flag used for configuring the behavior when the input value should be preferred + * over the value to inject. + * + * @since 2.20 + */ + protected final Boolean _useInput; + /** * @since 2.20 */ public ValueInjector(PropertyName propName, JavaType type, - AnnotatedMember mutator, Object valueId, Boolean optional) + AnnotatedMember mutator, Object valueId, Boolean optional, Boolean useInput) { super(propName, type, null, mutator, PropertyMetadata.STD_OPTIONAL); _valueId = valueId; _optional = optional; + _useInput = useInput; } /** @@ -47,20 +56,27 @@ public ValueInjector(PropertyName propName, JavaType type, public ValueInjector(PropertyName propName, JavaType type, AnnotatedMember mutator, Object valueId) { - this(propName, type, mutator, valueId, null); + this(propName, type, mutator, valueId, null, null); } public Object findValue(DeserializationContext context, Object beanInstance) throws JsonMappingException { - return context.findInjectableValue(_valueId, this, beanInstance, _optional); + return context.findInjectableValue(_valueId, this, beanInstance, _optional, _useInput); } public void inject(DeserializationContext context, Object beanInstance) throws IOException { final Object value = findValue(context, beanInstance); - if (!JacksonInject.Value.empty().equals(value)) { + + if (value == JacksonInject.Value.empty()) { + if (Boolean.FALSE.equals(_optional)) { + throw context.missingInjectableValueException(String.format( + "No 'injectableValues' configured, cannot inject value with id '%s'", _valueId), + _valueId, null, beanInstance); + } + } else if (!Boolean.TRUE.equals(_useInput)) { _member.setValue(beanInstance, value); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java index 3e76bda243..8bb0f5537d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java @@ -674,7 +674,8 @@ private Object _createUsingDelegate(AnnotatedWithParams delegateCreator, args[i] = delegate; } else { // nope, injectable: // 09-May-2025, tatu: Not sure where to get "optional" (last arg) value... - args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null, null); + args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null, null, + null); } } // and then try calling with full set of arguments diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java index 4826795da4..8648afc599 100644 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java @@ -699,7 +699,7 @@ static class TestInjector extends InjectableValues { @Override public Object findInjectableValue(DeserializationContext ctxt, Object valueId, - BeanProperty forProperty, Object beanInstance, Boolean optional) { + BeanProperty forProperty, Object beanInstance, Boolean optional, Boolean useInput) { if (valueId == "jjj") { UnreadableBean bean = new UnreadableBean(); bean.setValue(1); diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java b/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java index 473f089598..cc5e889882 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java @@ -40,12 +40,14 @@ public Object findInjectableValue( Object valueId, BeanProperty forProperty, Object beanInstance, - Boolean optional + Boolean optional, + Boolean useInput ) throws JsonMappingException { if (valueId.equals("id")) { return "id" + nextId++; } else { - return super.findInjectableValue(ctxt, valueId, forProperty, beanInstance, optional); + return super.findInjectableValue(ctxt, valueId, forProperty, beanInstance, + optional, useInput); } } }