Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9a42cca
feat(#1381): useInput=TRUE in JacksonInject now make deserialization …
giulong May 24, 2025
d714aeb
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Jun 5, 2025
d6bee5f
Change exception throws
cowtowncoder Jun 5, 2025
bc094d8
...
cowtowncoder Jun 5, 2025
a12a3d7
Add release notes; broke tests by changing annotation placements
cowtowncoder Jun 5, 2025
69c603e
test: minor refactor to apply coding conventions
giulong Jun 7, 2025
191292c
test(#1381): covering all cases for JacksonInject, whether we useInpu…
giulong Jun 15, 2025
5705aff
feat(#1381): JacksonInject.useInput works for constructor properties …
giulong Jun 15, 2025
bc9d37e
Merge branch '2.x' into feature/databind-1381-inject-only
giulong Jun 15, 2025
272c499
Merge branch 'FasterXML:2.x' into feature/databind-1381-inject-only
giulong Jun 20, 2025
5da46ac
test(#1381): minor code cleaning removing unused getters
giulong Jun 20, 2025
deb1741
feat(#1381): always adding properties annotated with JacksonInject to…
giulong Jun 20, 2025
bf34e81
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Jul 26, 2025
fac9b19
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 8, 2025
13715a3
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 22, 2025
03ba2cf
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 22, 2025
8423113
Add back accidentally removed CREDITS line (merge)
cowtowncoder Aug 22, 2025
98abe58
...
cowtowncoder Aug 22, 2025
402e3cb
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 22, 2025
bd2c4d7
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 26, 2025
0c2f3f3
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 26, 2025
202e5ec
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 26, 2025
e30140b
Test cleanup for easier 3.0 merging
cowtowncoder Aug 26, 2025
2da95a7
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 26, 2025
350f959
Minor renaming
cowtowncoder Aug 26, 2025
1e8578d
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Aug 27, 2025
32e84cb
Merge branch '2.x' into feature/databind-1381-inject-only
giulong Sep 3, 2025
5c58665
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Sep 6, 2025
5d32825
refactor(#1381): changing exception message in ValueInjector when no …
giulong Sep 6, 2025
285989f
chore(#1381): minor comment update
giulong Sep 6, 2025
eaf80fd
fix(#1381): avoid duplicate injection on constructor properties
giulong Sep 7, 2025
be8aea6
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Sep 12, 2025
a559e93
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Sep 19, 2025
6abb36e
Minor javadoc fix
cowtowncoder Sep 19, 2025
45fa152
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Sep 26, 2025
d258595
Fix name reference
cowtowncoder Sep 30, 2025
648b4bc
Improve test to verify injection field name
cowtowncoder Sep 30, 2025
e4f8d7d
Remove unnecessary diff
cowtowncoder Sep 30, 2025
807b6af
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Nov 6, 2025
8f905d8
Fix a bug
cowtowncoder Nov 6, 2025
8d76609
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Nov 10, 2025
6be9d07
Add parentheses for clarity
cowtowncoder Nov 10, 2025
193047f
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Nov 10, 2025
4defc99
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Nov 11, 2025
626e3c8
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Nov 11, 2025
0726035
Optimize tracking of injectable values
cowtowncoder Nov 11, 2025
0e780c0
...
cowtowncoder Nov 11, 2025
0991a05
...
cowtowncoder Nov 11, 2025
e18f0a3
...
cowtowncoder Nov 11, 2025
807771e
Merge branch '2.x' into feature/databind-1381-inject-only
cowtowncoder Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -1966,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
Expand Down
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
#1547: Un-deprecate `SerializationFeature.WRITE_EMPTY_JSON_ARRAYS`
#5045: If there is a no-parameter constructor marked as `JsonCreator` and
a constructor reported as `DefaultCreator`, latter is incorrectly used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,10 +469,12 @@ public final Object findInjectableValue(Object valueId,
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))) {
// `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)
Copy link
Member

@cowtowncoder cowtowncoder Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks odd; should second || actually be &&? Or maybe it's just odd grouping of clauses with parenthesis.

Should probably update comment to describe logic fully.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're returning empty() (we want to discard the injected value) in one of these cases:

  • either useInput or optional is true
  • either useInput or optional is null and FAIL_ON_UNKNOWN_INJECT_VALUE is disabled

maybe it's just odd grouping of clauses with parenthesis

Yup, due to formatting, I think the precedence in the last two lines is not immediately clear

if (Boolean.TRUE.equals(useInput)
        || Boolean.TRUE.equals(optional)
        || (useInput == null || optional == null)
        && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE)) {

I'm updating the comment

&& !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE)) {
return JacksonInject.Value.empty();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmh. This is wrong -- should be null, its Injectable value, NOT injection definition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 2.20, looks like; quietly returning new marker value. Should have caught this problem back then; I think I'll this now for 2.21.

}
throw missingInjectableValueException(String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ public Object getInjectableValueId() {
return (_injectableValue == null) ? null : _injectableValue.getId();
}

@Override // since 2.21
public JacksonInject.Value getInjectionDefinition() {
return _injectableValue;
}

@Override
public boolean isInjectionOnly() {
return (_injectableValue != null) && !_injectableValue.willUseInput(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -468,6 +469,14 @@ public int getCreatorIndex() {
*/
public Object getInjectableValueId() { return null; }

/**
* Accessor for injection definition, if this bean property supports
* value injection.
*
* @since 2.21
*/
public JacksonInject.Value getInjectionDefinition() { return null; }

/**
* Accessor for checking whether this property is injectable, and if so,
* ONLY injectable (will not bind from input).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -89,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
Expand All @@ -108,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)) {
Expand Down Expand Up @@ -218,6 +237,7 @@ public Object[] getParameters(SettableBeanProperty[] props)
if (_anyParamSetter != null) {
_creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue();
}

if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) {
for (int ix = 0; ix < props.length; ++ix) {
if (_creatorParameters[ix] == null) {
Expand All @@ -228,6 +248,19 @@ public Object[] getParameters(SettableBeanProperty[] props)
}
}
}

if (_paramsInjectedBig == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so now my remaining implementation problem is this: we should not

  1. Allocate _paramsInjectedBig if no injectable values are defined (to avoid overhead for common enough case) OR
  2. Do post-processing similarly (scanning over all properties for "small" case).

because I think it's not good to impose measurable overhead for non-injection cases.

To avoid these, checking for "are there any Injectables" would need to be done by pre-processing SettableBeanProperty set, I think, and passing a flag or something in PropertyValueBuffer constructor, because it cannot be done efficiently in getParameters().
Based on this flag, could eagerly construct _paramsInjectedBig.

I realize this is not trivial to do... but I feel it is important for performance.

As an extension, could create new helper class for holding Injectable status (BufferInjectables inner class?), and its existence could be used instead of flag.

for (int ix = 0; ix < _creatorParameters.length; ++ix) {
if ((_paramsInjected & 1) == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug! (thanks Claude :) ) -- needs to be

if ((_paramsInjected & (1 << ix)) == 0) {

Will fix.

_inject(props[ix]);
}
}
} else {
for (int ix = 0; (ix = _paramsInjectedBig.nextClearBit(ix)) < _creatorParameters.length; ++ix) {
_inject(props[ix]);
}
}

return _creatorParameters;
}

Expand Down Expand Up @@ -268,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);
}
Expand Down Expand Up @@ -302,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,15 @@ 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 injectable value with id '%s' found (for property '%s')",
_valueId, getName()),
_valueId, null, beanInstance);
}
} else if (!Boolean.TRUE.equals(_useInput)) {
_member.setValue(beanInstance, value);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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 = 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)
.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());
}
}
Loading