From 9a42cca8f55e012c7f829a9d69bfb25937d42379 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sat, 24 May 2025 13:22:50 +0200 Subject: [PATCH 01/26] 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 02/26] 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 03/26] ... --- .../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 04/26] 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 05/26] 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 06/26] 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 07/26] 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 08/26] 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 09/26] 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); } } } From 842311342dc8cdb70dbfb7f3c9ffb1d1a92ca3ab Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 22 Aug 2025 10:09:20 -0700 Subject: [PATCH 10/26] Add back accidentally removed CREDITS line (merge) --- release-notes/CREDITS-2.x | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 0c7da30a53..0406464b09 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1866,9 +1866,12 @@ wrongwrong (@k163377) * Contributed fix for #5139: In `CollectionDeserializer`, `JsonSetter.contentNulls` is sometimes ignored (2.19.1) - * Contributed fix for #5202: #5202: `JsonSetter.contentNulls` ignored for `Object[]`, + * Contributed fix for #5202: `JsonSetter.contentNulls` ignored for `Object[]`, `String[]` and `Collection` (2.19.2) + * Reported #4218: If `@JacksonInject` is specified for field and deserialized by + the Creator, the inject process will be executed twice + (2.20.0) Bernd Ahlers (@bernd) * Reported #4742: Deserialization with Builder, External type id, `@JsonCreator` failing From 98abe58234843210804d5c3ce917e5880f8253a5 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 22 Aug 2025 10:11:12 -0700 Subject: [PATCH 11/26] ... --- release-notes/VERSION-2.x | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 3a1d921a08..b1616724af 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,8 @@ Project: jackson-databind Not yet released +#1381: Add a way to specify "inject-only" with `@JacksonInject` + (fix by Giulio L) #2678: `@JacksonInject` added to property overrides value from the JSON even if `useInput` is `OptBoolean.TRUE` (reported by Plamen T) @@ -26,12 +28,6 @@ Not yet released 2.20.0-rc1 (04-Aug-2025) -#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) From e30140ba2494a1a8beb1cd40fe674b1dbc50dd9d Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 25 Aug 2025 20:38:37 -0700 Subject: [PATCH 12/26] Test cleanup for easier 3.0 merging --- ...nject1381DeserializationFeatureDisabledTest.java | 5 +++-- ...hOptionalDeserializationFeatureDisabledTest.java | 5 +++-- .../databind/deser/inject/TestInjectables.java | 13 ++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) 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 index 48ccc42a32..ca0ea33a4f 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java @@ -107,8 +107,9 @@ public String getField() { 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 plainMapper = jsonMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) + .build(); private final ObjectMapper injectedMapper = jsonMapperBuilder() .injectableValues(new InjectableValues.Std().addValue("key", "injected")) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) 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 index 0d173a8fde..bfc88d184f 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java @@ -114,8 +114,9 @@ public String getField() { 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 plainMapper = jsonMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) + .build(); private final ObjectMapper injectedMapper = jsonMapperBuilder() .injectableValues(new InjectableValues.Std().addValue("key", "injected")) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java index 52199aa2f2..47cfb5be5b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java @@ -5,13 +5,11 @@ import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; import static org.junit.jupiter.api.Assertions.*; -import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.a2q; -import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.newJsonMapper; - -public class TestInjectables +public class TestInjectables extends DatabindTestUtil { static class InjectedBean { @@ -104,12 +102,13 @@ public void setMethodValue(String methodValue) { @Test public void testSimple() throws Exception { - ObjectMapper mapper = newJsonMapper(); - mapper.setInjectableValues(new InjectableValues.Std() + ObjectMapper mapper = jsonMapperBuilder() + .injectableValues(new InjectableValues.Std() .addValue(String.class, "stuffValue") .addValue("myId", "xyz") .addValue(Long.TYPE, Long.valueOf(37)) - ); + ) + .build(); InjectedBean bean = mapper.readValue("{\"value\":3}", InjectedBean.class); assertEquals(3, bean.value); assertEquals("stuffValue", bean.stuff); From 350f9592091f07935cc8fe6c7e7fa44da69b7ee5 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 25 Aug 2025 20:49:16 -0700 Subject: [PATCH 13/26] Minor renaming --- .../jackson/databind/deser/CreatorProperty.java | 4 ++-- .../jackson/databind/deser/SettableBeanProperty.java | 4 ++-- .../databind/deser/impl/PropertyValueBuffer.java | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) 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 c5a9df0395..1992ffb267 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java @@ -280,8 +280,8 @@ public Object getInjectableValueId() { return (_injectableValue == null) ? null : _injectableValue.getId(); } - @Override - public JacksonInject.Value getInjectableValue() { + @Override // since 2.20 + public JacksonInject.Value getInjectionDefinition() { return _injectableValue; } 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 20329c8be9..2ed2200508 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java @@ -470,12 +470,12 @@ public int getCreatorIndex() { public Object getInjectableValueId() { return null; } /** - * Accessor for injectable value, if this bean property supports + * Accessor for injection definition, if this bean property supports * value injection. * * @since 2.20 */ - public JacksonInject.Value getInjectableValue() { return null; } + public JacksonInject.Value getInjectionDefinition() { return null; } /** * Accessor for checking whether this property is injectable, and if so, 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 6dd286c240..5fde6eeaec 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,14 +222,14 @@ public Object[] getParameters(SettableBeanProperty[] props) for (int ix = 0; ix < props.length; ++ix) { final SettableBeanProperty prop = props[ix]; - final JacksonInject.Value injectableValue = prop.getInjectableValue(); + final JacksonInject.Value injection = prop.getInjectionDefinition(); - if (injectableValue != null) { - final Boolean useInput = injectableValue.getUseInput(); + if (injection != null) { + final Boolean useInput = injection.getUseInput(); if (!Boolean.TRUE.equals(useInput)) { - final Object value = _context.findInjectableValue(injectableValue.getId(), - prop, prop.getMember(), injectableValue.getOptional(), useInput); + final Object value = _context.findInjectableValue(injection.getId(), + prop, prop.getMember(), injection.getOptional(), useInput); if (value != JacksonInject.Value.empty()) { _creatorParameters[ix] = value; From 5d3282509e9b7854d72d4456bb4a76a2e35cede2 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sat, 6 Sep 2025 18:02:18 +0200 Subject: [PATCH 14/26] refactor(#1381): changing exception message in ValueInjector when no value with the provided id is found --- .../fasterxml/jackson/databind/deser/impl/ValueInjector.java | 5 +++-- .../jackson/databind/deser/inject/JacksonInject3072Test.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) 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 d8c6d057c7..f18b2935dd 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 @@ -72,8 +72,9 @@ public void inject(DeserializationContext context, Object beanInstance) 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), + throw context.missingInjectableValueException( + String.format("No injectable value with id '%s' found (for property '%s')", + _valueId, beanInstance), _valueId, null, beanInstance); } } else if (!Boolean.TRUE.equals(_useInput)) { 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 a5d399de31..9f169c8c82 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 @@ -76,7 +76,7 @@ void testRequiredAnnotatedField() throws Exception { MissingInjectableValueExcepion.class, () -> reader.readValue("{}")); assertThat(exception.getMessage()) - .startsWith("No 'injectableValues' configured, cannot inject value with id 'requiredValue'"); + .startsWith("No injectable value with id 'requiredValue' found (for property "); // Also check the other code path, with non-null Injectables ObjectReader reader2 = reader.with(new InjectableValues.Std() From 285989fc9c42e536abd9294d3f09da2935a5d53e Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sat, 6 Sep 2025 18:03:28 +0200 Subject: [PATCH 15/26] chore(#1381): minor comment update --- .../fasterxml/jackson/databind/DeserializationContext.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index a0bf455b6a..bfd3db28d6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -469,8 +469,8 @@ public final Object findInjectableValue(Object valueId, throws JsonMappingException { if (_injectableValues == null) { - // `optional` comes from property annotation (if any); has precedence - // over global setting. + // `useInput` and `optional` come from property annotation (if any); + // they have precedence over global setting. if (Boolean.TRUE.equals(useInput) || Boolean.TRUE.equals(optional) || (useInput == null || optional == null) From eaf80fd86805f6b624368a817b5d18e81b1cc16f Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sun, 7 Sep 2025 19:12:12 +0200 Subject: [PATCH 16/26] fix(#1381): avoid duplicate injection on constructor properties --- .../deser/impl/PropertyValueBuffer.java | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) 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 5fde6eeaec..f16796931f 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 @@ -90,6 +90,22 @@ public class PropertyValueBuffer */ protected PropertyValue _anyParamBuffered; + /** + * Bitflag used to track already injected parameters when number of parameters is + * less than 32 (fits in int). + * + * @since 2.21 + */ + protected int _paramsInjected; + + /** + * Bitflag used to track already injected parameters when number of parameters is + * 32 or higher. + * + * @since 2.21 + */ + protected final BitSet _paramsInjectedBig; + /* /********************************************************** /* Life-cycle @@ -109,8 +125,10 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC _creatorParameters = new Object[paramCount]; if (paramCount < 32) { _paramsSeenBig = null; + _paramsInjectedBig = null; } else { _paramsSeenBig = new BitSet(); + _paramsInjectedBig = new BitSet(); } // Only care about Creator-bound Any setters: if ((anyParamSetter == null) || (anyParamSetter.getParameterIndex() < 0)) { @@ -220,24 +238,6 @@ public Object[] getParameters(SettableBeanProperty[] props) _creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue(); } - for (int ix = 0; ix < props.length; ++ix) { - final SettableBeanProperty prop = props[ix]; - final JacksonInject.Value injection = prop.getInjectionDefinition(); - - if (injection != null) { - final Boolean useInput = injection.getUseInput(); - - if (!Boolean.TRUE.equals(useInput)) { - final Object value = _context.findInjectableValue(injection.getId(), - prop, prop.getMember(), injection.getOptional(), useInput); - - if (value != JacksonInject.Value.empty()) { - _creatorParameters[ix] = value; - } - } - } - } - if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { if (_creatorParameters[ix] == null) { @@ -248,6 +248,19 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } + + if (_paramsInjectedBig == null) { + for (int ix = 0; ix < _creatorParameters.length; ++ix) { + if ((_paramsInjected & 1) == 0) { + _inject(props[ix]); + } + } + } else { + for (int ix = 0; (ix = _paramsInjectedBig.nextClearBit(ix)) < _creatorParameters.length; ++ix) { + _inject(props[ix]); + } + } + return _creatorParameters; } @@ -288,6 +301,7 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep // First: do we have injectable value? Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { + _trackInjected(prop); return _context.findInjectableValue(prop.getInjectableValueId(), prop, null, null, null); } @@ -322,6 +336,32 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep } } + private void _inject(final SettableBeanProperty prop) throws JsonMappingException { + final JacksonInject.Value injection = prop.getInjectionDefinition(); + + if (injection != null) { + final Boolean useInput = injection.getUseInput(); + + if (!Boolean.TRUE.equals(useInput)) { + final Object value = _context.findInjectableValue(injection.getId(), + prop, prop.getMember(), injection.getOptional(), useInput); + + if (value != JacksonInject.Value.empty()) { + _trackInjected(prop); + _creatorParameters[prop.getCreatorIndex()] = value; + } + } + } + } + + private void _trackInjected(final SettableBeanProperty prop) { + if (_paramsInjectedBig == null) { + _paramsInjected |= 1 << prop.getCreatorIndex(); + } else { + _paramsInjectedBig.set(prop.getCreatorIndex()); + } + } + /* /********************************************************** /* Other methods From 6abb36e4c592ab78b490e527dbdbe5647b3c7699 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 19 Sep 2025 16:51:20 -0700 Subject: [PATCH 17/26] Minor javadoc fix --- release-notes/CREDITS-2.x | 4 ++-- release-notes/VERSION-2.x | 4 ++-- .../com/fasterxml/jackson/databind/deser/CreatorProperty.java | 2 +- .../jackson/databind/deser/SettableBeanProperty.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 892cd3c5bc..fbe2b55b10 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1957,8 +1957,6 @@ EddĂș MelĂ©ndez Gonzales (@eddumelendez) (2.19.2) 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) @@ -1968,6 +1966,8 @@ Giulio Longfils (@giulong) * Contributed #4218: If `@JacksonInject` is specified for field and deserialized by the Creator, the inject process will be executed twice (2.20.0) + * Contributed #1381: Add a way to specify "inject-only" with `@JacksonInject` + (2.21.0) Plamen Tanov (@ptanov) * Reported #2678: `@JacksonInject` added to property overrides value from the JSON diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 8748f3662d..056483f114 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,8 @@ Project: jackson-databind 2.21.0 (not yet released) +#1381: Add a way to specify "inject-only" with `@JacksonInject` + (fix by Giulio L) #5045: If there is a no-parameter constructor marked as `JsonCreator` and a constructor reported as `DefaultCreator`, latter is incorrectly used (reported by @wrongwrong) @@ -23,8 +25,6 @@ Project: jackson-databind 2.20.0 (28-Aug-2025) -#1381: Add a way to specify "inject-only" with `@JacksonInject` - (fix by Giulio L) #2678: `@JacksonInject` added to property overrides value from the JSON even if `useInput` is `OptBoolean.TRUE` (reported by Plamen T) 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 1992ffb267..9ce62e6472 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java @@ -280,7 +280,7 @@ public Object getInjectableValueId() { return (_injectableValue == null) ? null : _injectableValue.getId(); } - @Override // since 2.20 + @Override // since 2.21 public JacksonInject.Value getInjectionDefinition() { return _injectableValue; } 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 2ed2200508..f08a481379 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java @@ -473,7 +473,7 @@ public int getCreatorIndex() { * Accessor for injection definition, if this bean property supports * value injection. * - * @since 2.20 + * @since 2.21 */ public JacksonInject.Value getInjectionDefinition() { return null; } From d25859582143cd3c55cae0ffcc2b987993fa0135 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 29 Sep 2025 19:20:22 -0700 Subject: [PATCH 18/26] Fix name reference --- .../fasterxml/jackson/databind/deser/impl/ValueInjector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f18b2935dd..2a6b3bd7c2 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 @@ -74,7 +74,7 @@ public void inject(DeserializationContext context, Object beanInstance) if (Boolean.FALSE.equals(_optional)) { throw context.missingInjectableValueException( String.format("No injectable value with id '%s' found (for property '%s')", - _valueId, beanInstance), + _valueId, getName()), _valueId, null, beanInstance); } } else if (!Boolean.TRUE.equals(_useInput)) { From 648b4bcb1a1380a3728f1217866b1e8902557764 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 29 Sep 2025 19:26:39 -0700 Subject: [PATCH 19/26] Improve test to verify injection field name --- .../jackson/databind/deser/inject/JacksonInject3072Test.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9f169c8c82..cf2b0c0301 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 @@ -76,7 +76,7 @@ void testRequiredAnnotatedField() throws Exception { MissingInjectableValueExcepion.class, () -> reader.readValue("{}")); assertThat(exception.getMessage()) - .startsWith("No injectable value with id 'requiredValue' found (for property "); + .startsWith("No injectable value with id 'requiredValue' found (for property 'requiredField')"); // Also check the other code path, with non-null Injectables ObjectReader reader2 = reader.with(new InjectableValues.Std() From e4f8d7d3fa76188b255141830ce64bcab3751910 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 29 Sep 2025 19:46:35 -0700 Subject: [PATCH 20/26] Remove unnecessary diff --- .../jackson/databind/deser/std/StdValueInstantiator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e6a724fc5b..1e578e0d7d 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 @@ -675,8 +675,8 @@ private Object _createUsingDelegate(AnnotatedWithParams delegateCreator, } else { // nope, injectable: // 09-May-2025, tatu: Not sure where to get "optional" (last arg) value... // 25-Aug-2025, tatu: ... or "useInput" - args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null, - null, null); + args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, + null, null, null); } } // and then try calling with full set of arguments From 8f905d838599ba0dc9583bed5766e60bc99551ea Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 5 Nov 2025 20:06:33 -0800 Subject: [PATCH 21/26] Fix a bug --- .../jackson/databind/deser/impl/PropertyValueBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f16796931f..8d9d478e0a 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 @@ -251,7 +251,7 @@ public Object[] getParameters(SettableBeanProperty[] props) if (_paramsInjectedBig == null) { for (int ix = 0; ix < _creatorParameters.length; ++ix) { - if ((_paramsInjected & 1) == 0) { + if ((_paramsInjected & (1 << ix)) == 0) { _inject(props[ix]); } } From 6be9d07472144c385304c64cea2a018f4a75229b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 9 Nov 2025 17:27:19 -0800 Subject: [PATCH 22/26] Add parentheses for clarity --- .../fasterxml/jackson/databind/DeserializationContext.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index bfd3db28d6..39e359aaa6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -473,8 +473,8 @@ public final Object findInjectableValue(Object valueId, // they have precedence over global setting. if (Boolean.TRUE.equals(useInput) || Boolean.TRUE.equals(optional) - || (useInput == null || optional == null) - && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE)) { + || ((useInput == null || optional == null) + && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) { return JacksonInject.Value.empty(); } throw missingInjectableValueException(String.format( From 0726035ccd397aba28f41833c1df53cdd035210a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 10 Nov 2025 19:30:26 -0800 Subject: [PATCH 23/26] Optimize tracking of injectable values --- .../deser/impl/PropertyValueBuffer.java | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) 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 0f2a005615..348d2929bb 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 @@ -90,23 +90,6 @@ public class PropertyValueBuffer */ protected PropertyValue _anyParamBuffered; - /** -<<<<<<< HEAD - * Bitflag used to track already injected parameters when number of parameters is - * less than 32 (fits in int). - * - * @since 2.21 - */ - protected int _paramsInjected; - - /** - * Bitflag used to track already injected parameters when number of parameters is - * 32 or higher. - * - * @since 2.21 - */ - protected final BitSet _paramsInjectedBig; - /** * Indexes properties that are injectable, if any; {@code null} if none. * @@ -134,10 +117,8 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC _creatorParameters = new Object[paramCount]; if (paramCount < 32) { _paramsSeenBig = null; - _paramsInjectedBig = null; } else { _paramsSeenBig = new BitSet(); - _paramsInjectedBig = new BitSet(); } // Only care about Creator-bound Any setters: if ((anyParamSetter == null) || (anyParamSetter.getParameterIndex() < 0)) { @@ -251,7 +232,8 @@ public Object[] getParameters(SettableBeanProperty[] props) } if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { - for (int ix = 0; ix < props.length; ++ix) { + final int len = _creatorParameters.length; + for (int ix = 0; ix < len; ++ix) { if (_creatorParameters[ix] == null) { SettableBeanProperty prop = props[ix]; _context.reportInputMismatch(prop, @@ -261,15 +243,12 @@ public Object[] getParameters(SettableBeanProperty[] props) } } - if (_paramsInjectedBig == null) { - for (int ix = 0; ix < _creatorParameters.length; ++ix) { - if ((_paramsInjected & (1 << ix)) == 0) { - _inject(props[ix]); - } - } - } else { - for (int ix = 0; (ix = _paramsInjectedBig.nextClearBit(ix)) < _creatorParameters.length; ++ix) { + if (_injectablePropIndexes != null) { + int ix = _injectablePropIndexes.nextSetBit(0); + + while (ix >= 0) { _inject(props[ix]); + ix = _injectablePropIndexes.nextSetBit(ix + 1); } } @@ -367,10 +346,8 @@ private void _inject(final SettableBeanProperty prop) throws JsonMappingExceptio } private void _trackInjected(final SettableBeanProperty prop) { - if (_paramsInjectedBig == null) { - _paramsInjected |= 1 << prop.getCreatorIndex(); - } else { - _paramsInjectedBig.set(prop.getCreatorIndex()); + if (_injectablePropIndexes != null) { + _injectablePropIndexes.clear(prop.getCreatorIndex()); } } From 0e780c055434d65884077f34ba10f4feaf5b4b7a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 10 Nov 2025 19:48:09 -0800 Subject: [PATCH 24/26] ... --- .../deser/impl/PropertyValueBuffer.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) 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 348d2929bb..ce0ebb1464 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 @@ -231,6 +231,15 @@ public Object[] getParameters(SettableBeanProperty[] props) _creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue(); } + // [databind#1381] handle inject-only (useInput = false) properties + if (_injectablePropIndexes != null) { + int ix = _injectablePropIndexes.nextSetBit(0); + while (ix >= 0) { + _inject(props[ix]); + ix = _injectablePropIndexes.nextSetBit(ix + 1); + } + } + if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { final int len = _creatorParameters.length; for (int ix = 0; ix < len; ++ix) { @@ -243,15 +252,6 @@ public Object[] getParameters(SettableBeanProperty[] props) } } - if (_injectablePropIndexes != null) { - int ix = _injectablePropIndexes.nextSetBit(0); - - while (ix >= 0) { - _inject(props[ix]); - ix = _injectablePropIndexes.nextSetBit(ix + 1); - } - } - return _creatorParameters; } @@ -292,7 +292,8 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep // First: do we have injectable value? Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { - _trackInjected(prop); + // 10-Nov-2025: [databind#1381] Is this needed? + _injectablePropIndexes.clear(prop.getCreatorIndex()); return _context.findInjectableValue(prop.getInjectableValueId(), prop, null, null, null); } @@ -338,19 +339,14 @@ private void _inject(final SettableBeanProperty prop) throws JsonMappingExceptio prop, prop.getMember(), injection.getOptional(), useInput); if (value != JacksonInject.Value.empty()) { - _trackInjected(prop); - _creatorParameters[prop.getCreatorIndex()] = value; + int ix = prop.getCreatorIndex(); + _creatorParameters[ix] = value; + _injectablePropIndexes.clear(ix); } } } } - private void _trackInjected(final SettableBeanProperty prop) { - if (_injectablePropIndexes != null) { - _injectablePropIndexes.clear(prop.getCreatorIndex()); - } - } - /* /********************************************************** /* Other methods @@ -430,6 +426,7 @@ public boolean assignParameter(SettableBeanProperty prop, Object value) _paramsSeenBig.set(ix); if (--_paramsNeeded <= 0) { // 29-Nov-2016, tatu: But! May still require Object Id value + return (_objectIdReader == null) || (_idValue != null); } } } From 0991a057b98ee500275e4bc3034cd6186c44a9ff Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 10 Nov 2025 20:20:45 -0800 Subject: [PATCH 25/26] ... --- attic/ImmutableBitSet.java | 85 +++++++++++++++++++ .../deser/impl/PropertyValueBuffer.java | 10 ++- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 attic/ImmutableBitSet.java diff --git a/attic/ImmutableBitSet.java b/attic/ImmutableBitSet.java new file mode 100644 index 0000000000..796213a550 --- /dev/null +++ b/attic/ImmutableBitSet.java @@ -0,0 +1,85 @@ +package com.fasterxml.jackson.databind.util; + +import java.util.BitSet; + +public class ImmutableBitSet extends BitSet +{ + private static final long serialVersionUID = 1L; + + private ImmutableBitSet(BitSet bits) { + super(); + _parentOr(bits); + } + + public static ImmutableBitSet of(BitSet bits) { + return new ImmutableBitSet(bits); + } + + private void _parentOr(BitSet set) { + super.or(set); + } + + @Override + public void and(BitSet set) { + _failMutableOperation(); + } + + @Override + public void andNot(BitSet set) { + _failMutableOperation(); + } + + @Override + public void or(BitSet set) { + _failMutableOperation(); + } + + @Override + public void xor(BitSet set) { + _failMutableOperation(); + } + + @Override + public void clear() { + _failMutableOperation(); + } + + @Override + public void clear(int ix) { + _failMutableOperation(); + } + + @Override + public void clear(int from, int to) { + _failMutableOperation(); + } + + @Override + public void flip(int bitIndex) { + _failMutableOperation(); + } + + @Override + public void flip(int from, int to) { + _failMutableOperation(); + } + + @Override + public void set(int bitIndex) { + _failMutableOperation(); + } + + @Override + public void set(int bitIndex, boolean state) { + _failMutableOperation(); + } + + @Override + public void set(int from, int to) { + _failMutableOperation(); + } + + private void _failMutableOperation() { + throw new UnsupportedOperationException("ImmutableBitSet does not support modification"); + } +} 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 ce0ebb1464..0c020ad830 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 @@ -91,7 +91,8 @@ public class PropertyValueBuffer protected PropertyValue _anyParamBuffered; /** - * Indexes properties that are injectable, if any; {@code null} if none. + * Indexes properties that are injectable, if any; {@code null} if none, + * cleared as they are injected. * * @since 2.21 */ @@ -328,6 +329,12 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep } } + /** + * Method called to inject value for given property, possibly overriding + * assigned (from input) value. + * + * @since 2.21 + */ private void _inject(final SettableBeanProperty prop) throws JsonMappingException { final JacksonInject.Value injection = prop.getInjectionDefinition(); @@ -341,7 +348,6 @@ private void _inject(final SettableBeanProperty prop) throws JsonMappingExceptio if (value != JacksonInject.Value.empty()) { int ix = prop.getCreatorIndex(); _creatorParameters[ix] = value; - _injectablePropIndexes.clear(ix); } } } From e18f0a386f9b00802aa476ff6c8624869b2cdeb8 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 10 Nov 2025 20:22:44 -0800 Subject: [PATCH 26/26] ... --- attic/ImmutableBitSet.java | 85 -------------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 attic/ImmutableBitSet.java diff --git a/attic/ImmutableBitSet.java b/attic/ImmutableBitSet.java deleted file mode 100644 index 796213a550..0000000000 --- a/attic/ImmutableBitSet.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.fasterxml.jackson.databind.util; - -import java.util.BitSet; - -public class ImmutableBitSet extends BitSet -{ - private static final long serialVersionUID = 1L; - - private ImmutableBitSet(BitSet bits) { - super(); - _parentOr(bits); - } - - public static ImmutableBitSet of(BitSet bits) { - return new ImmutableBitSet(bits); - } - - private void _parentOr(BitSet set) { - super.or(set); - } - - @Override - public void and(BitSet set) { - _failMutableOperation(); - } - - @Override - public void andNot(BitSet set) { - _failMutableOperation(); - } - - @Override - public void or(BitSet set) { - _failMutableOperation(); - } - - @Override - public void xor(BitSet set) { - _failMutableOperation(); - } - - @Override - public void clear() { - _failMutableOperation(); - } - - @Override - public void clear(int ix) { - _failMutableOperation(); - } - - @Override - public void clear(int from, int to) { - _failMutableOperation(); - } - - @Override - public void flip(int bitIndex) { - _failMutableOperation(); - } - - @Override - public void flip(int from, int to) { - _failMutableOperation(); - } - - @Override - public void set(int bitIndex) { - _failMutableOperation(); - } - - @Override - public void set(int bitIndex, boolean state) { - _failMutableOperation(); - } - - @Override - public void set(int from, int to) { - _failMutableOperation(); - } - - private void _failMutableOperation() { - throw new UnsupportedOperationException("ImmutableBitSet does not support modification"); - } -}