Skip to content

Support single-Map @JsonAnySetter methods - #6075

Open
goutamadwant wants to merge 8 commits into
FasterXML:3.xfrom
goutamadwant:fix-anysetter-map-method
Open

Support single-Map @JsonAnySetter methods#6075
goutamadwant wants to merge 8 commits into
FasterXML:3.xfrom
goutamadwant:fix-anysetter-map-method

Conversation

@goutamadwant

@goutamadwant goutamadwant commented Jul 7, 2026

Copy link
Copy Markdown

Fixes #4889

Root cause

@JsonAnySetter discovery handled fields, creator parameters, and two-argument setter methods, but explicitly annotated one-argument methods were still treated as regular setters. That meant a method like setOther(Map<String, Object> other) was not recognized as an any-setter target.

Change

This adds support for explicitly annotated single-argument Map any-setter methods. Unknown properties are collected into a map using the existing any-setter key/value deserialization flow, then the annotated method is invoked once with the collected values.

Verification

  • ./mvnw -q -Dtest=tools.jackson.databind.deser.AnySetterTest#testJsonAnySetterOnMapMethod test
  • ./mvnw -q -Dtest=tools.jackson.databind.deser.AnySetterTest test
  • ./mvnw -q -Dtest=tools.jackson.databind.deser.AnySetterTest,tools.jackson.databind.deser.builder.BuilderSimpleTest,tools.jackson.databind.deser.creators.AnySetterForCreator562Test,tools.jackson.databind.introspect.POJOPropertiesCollectorTest test
  • ./mvnw -q test
  • ./mvnw -q -DskipTests animal-sniffer:check
  • git diff --check

@JooHyukKim

Copy link
Copy Markdown
Member

Can you specify issue number on test methods? @goutamadwant See neighboring test cases as example

@goutamadwant
goutamadwant force-pushed the fix-anysetter-map-method branch from 75278f1 to 41e77d2 Compare July 8, 2026 06:40
@goutamadwant

Copy link
Copy Markdown
Author

Can you specify issue number on test methods? @goutamadwant See neighboring test cases as example

Thanks @JooHyukKim, updated the new test method names to include 4889, matching the neighboring test cases. Let me know if any suggestions. thanks!

@github-actions

Copy link
Copy Markdown

🧪 Code Coverage Report

Metric Coverage Change
Instructions coverage 81.70% 📉 -0.010%
Branches branches 75.19% 📉 -0.040%

Coverage data generated from JaCoCo test results

@cowtowncoder cowtowncoder changed the title Support single-Map @JsonAnySetter methods Support single-Map @JsonAnySetter methods Jul 10, 2026

@JooHyukKim JooHyukKim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need JavaDocs as well.
Thanks!

if (anyMethod.getParameterCount() == 1) {
Class<?> type = anyMethod.getRawParameterType(0);
if (!Map.class.isAssignableFrom(type)) {
throw new IllegalArgumentException(String.format(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure if we can simply throw error, considering we are doing minor version fix...? 🤔

throw new UnsupportedOperationException("Cannot call createParameterObject() on " + getClass().getName());
}

public void finish(DeserializationContext ctxt, Object instance) throws JacksonException { }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Method name is generic too much.
Can we do both below?

  1. Change name
  2. Add JavaDoc

I'm talking about all finish methods

@cowtowncoder cowtowncoder added the cla-needed PR looks good (although may also require code review), but CLA needed from submitter label Jul 23, 2026
@github-actions

Copy link
Copy Markdown

🧪 Code Coverage Report

Metric Coverage Change
Instructions coverage 81.79% 📈 +0.010%
Branches branches 75.40% 📈 +0.010%

Coverage data generated from JaCoCo test results

@cowtowncoder

cowtowncoder commented Jul 23, 2026

Copy link
Copy Markdown
Member

Thank you @goutamadwant ! One more thing we need is CLA, from:

https://github.com/FasterXML/jackson/blob/main/CLA-jackson-2026.pdf

(only needs to be sent once)
which is usually down by printing it, filling & signing, scan/photo, email to cla at fasterxml dot com.

@cowtowncoder

Copy link
Copy Markdown
Member

Hmmmmh. This is WAY bigger change than I'd expect. I wonder if there is some refactoring that could be done to simplify implementation?

Comment thread src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java
Comment on lines +94 to +108
public static SettableAnyProperty constructForMapMethod(DeserializationContext ctxt,
BeanProperty property,
AnnotatedMember setter, JavaType valueType,
KeyDeserializer keyDeser,
ValueDeserializer<Object> valueDeser, TypeDeserializer typeDeser)
{
Class<?> mapType = property.getType().getRawClass();
if (mapType == Map.class) {
mapType = LinkedHashMap.class;
}
ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType);
return new MapMethodAnyProperty(property, setter, valueType,
keyDeser, valueDeser, typeDeser,
vi);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Map any-setter with non-instantiable concrete Map fails late

constructForMapMethod only remaps the raw Map.class interface to LinkedHashMap; for other non-instantiable declared types (e.g. SortedMap, Map.Entry-less abstract maps, or a concrete type without a default constructor) findStdValueInstantiator returns null and _createMap throws a DatabindException only at deserialization time when the first unknown property arrives. Consider validating instantiability at introspection/construction time (like other constructForMap* helpers already have this same limitation) so misconfiguration surfaces earlier, and/or mapping common interfaces such as SortedMap/NavigableMap to TreeMap.

Was this helpful? React with 👍 / 👎

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would be helpful to verify this case

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added a test verifying that an unsupported custom Map type reports the expected DatabindException when an unknown property is encountered.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🧪 Code Coverage Report

Metric Coverage Change
Instructions coverage 81.91% 📈 +0.010%
Branches branches 75.52% 📈 +0.020%

Coverage data generated from JaCoCo test results

@goutamadwant

Copy link
Copy Markdown
Author

Thank you @goutamadwant ! One more thing we need is CLA, from:

https://github.com/FasterXML/jackson/blob/main/CLA-jackson-2026.pdf

(only needs to be sent once) which is usually down by printing it, filling & signing, scan/photo, email to cla at fasterxml dot com.

done @cowtowncoder I emailed the pdf to the provided address.

@goutamadwant

Copy link
Copy Markdown
Author

Thanks for the review @cowtowncoder @JooHyukKim looked at all the comments and pushed a follow up update as below.

  • renames the generic completion hooks to finishAnySetter(...) and adds Javadocs
  • keeps single-argument type validation in BeanDeserializerFactory instead of throwing during bean introspection
  • simplifies the deferred map storage by removing the composite key class
  • verifies that only explicitly annotated one-argument methods are treated as any-setters
  • adds coverage for active views, property-based creators with external type ids, no unknown properties, regular Map setters, and builder deserialization
  • updates the builder test name to include issue 4889

The no-unknown-properties behavior remains consistent with existing two-argument any-setters. Support for additional Map interfaces was left out because the same limitation exists for Map-field any-setters and would be better handled separately.

let me know if there any other suggestions or comments. thanks!

@JooHyukKim JooHyukKim left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Added a lil more reviews, hope it helps!

if (am.getParameterCount() == 1) {
JavaType paramType = am.getParameterType(0);
if (!paramType.isMapLikeType()) {
return ctxt.reportBadDefinition(beanDescRef.getType(), String.format(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it really possible to reach this code path? If so, this would throw error --backward-incompatible

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

nice point @JooHyukKim only one-argument Map methods are now registered as any-setters. Non-Map methods remain regular setters also has a regression test covering this behavior.

Comment on lines +94 to +108
public static SettableAnyProperty constructForMapMethod(DeserializationContext ctxt,
BeanProperty property,
AnnotatedMember setter, JavaType valueType,
KeyDeserializer keyDeser,
ValueDeserializer<Object> valueDeser, TypeDeserializer typeDeser)
{
Class<?> mapType = property.getType().getRawClass();
if (mapType == Map.class) {
mapType = LinkedHashMap.class;
}
ValueInstantiator vi = JDKValueInstantiators.findStdValueInstantiator(ctxt.getConfig(), mapType);
return new MapMethodAnyProperty(property, setter, valueType,
keyDeser, valueDeser, typeDeser,
vi);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would be helpful to verify this case

Comment on lines 1205 to 1218

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just style suggestion so quite subjective but, how about isolate into new _addSetterMethod() method like we have for arg count 0 case?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done @JooHyukKim one-argument methods now delegate to _addSetterMethod(), which handles Map any-setter classification before regular setter discovery.

@gitar-bot

gitar-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 2 resolved / 3 findings

Adds support for single-argument Map @JsonAnySetter methods with comprehensive test coverage. Consider adding early validation in constructForMapMethod to catch non-instantiable concrete map types before runtime execution.

💡 Edge Case: Map any-setter with non-instantiable concrete Map fails late

📄 src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java:94-108 📄 src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java:462-470

constructForMapMethod only remaps the raw Map.class interface to LinkedHashMap; for other non-instantiable declared types (e.g. SortedMap, Map.Entry-less abstract maps, or a concrete type without a default constructor) findStdValueInstantiator returns null and _createMap throws a DatabindException only at deserialization time when the first unknown property arrives. Consider validating instantiability at introspection/construction time (like other constructForMap* helpers already have this same limitation) so misconfiguration surfaces earlier, and/or mapping common interfaces such as SortedMap/NavigableMap to TreeMap.

✅ 2 resolved
Edge Case: Map any-setter may be skipped in view/external-id paths

📄 src/main/java/tools/jackson/databind/deser/bean/BeanDeserializer.java:608-615 📄 src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java:444-458
The new _finishAnySetter(...) flush was added to the vanilla, unwrapped, and standard property-based paths, but it is not obviously present in every terminal return path (e.g. deserializeWithView, and the property-based external-type-id path deserializeUsingPropertyBasedWithExternalTypeId). Because a single-Map @JsonAnySetter method now only collects entries into a context-attribute map during _set and is invoked exclusively from finish(...), any deserialization path that returns the bean without calling _finishAnySetter will silently drop all collected unknown properties and never invoke the annotated method. Please verify each return bean path that can process unknown properties (@JsonView-active beans, external type id) routes through _finishAnySetter, and add tests covering a Map any-setter method combined with @JsonView.

Edge Case: Map any-setter method not invoked when no unknown props

📄 src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java:430-444
For a single-Map @JsonAnySetter method, _set is only called for unknown properties, so when a payload has no unknown properties the context-attribute map is never created and finish(...) returns early without invoking the annotated method. The target field therefore stays at its default (e.g. null) rather than being set to an empty map. This matches the two-argument any-setter behavior, but if callers expect the method to always be invoked (e.g. to initialize an empty map), document the behavior or invoke the setter with an empty map. Consider adding a test asserting the no-unknowns case.

🤖 Prompt for agents
Code Review: Adds support for single-argument Map `@JsonAnySetter` methods with comprehensive test coverage. Consider adding early validation in `constructForMapMethod` to catch non-instantiable concrete map types before runtime execution.

1. 💡 Edge Case: Map any-setter with non-instantiable concrete Map fails late
   Files: src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java:94-108, src/main/java/tools/jackson/databind/deser/SettableAnyProperty.java:462-470

   `constructForMapMethod` only remaps the raw `Map.class` interface to `LinkedHashMap`; for other non-instantiable declared types (e.g. `SortedMap`, `Map.Entry`-less abstract maps, or a concrete type without a default constructor) `findStdValueInstantiator` returns null and `_createMap` throws a `DatabindException` only at deserialization time when the first unknown property arrives. Consider validating instantiability at introspection/construction time (like other constructForMap* helpers already have this same limitation) so misconfiguration surfaces earlier, and/or mapping common interfaces such as `SortedMap`/`NavigableMap` to `TreeMap`.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@goutamadwant goutamadwant left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

addressed all the review comments. thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-needed PR looks good (although may also require code review), but CLA needed from submitter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support @JsonAnySetter on setter method (single Map parameter)

3 participants