Skip to content

WW-5701 fix(conversion): compare the conversion marker by identity, not equals - #1874

Merged
lukaszlenart merged 2 commits into
mainfrom
fix/WW-5701-collection-converter-identity
Aug 31, 2026
Merged

WW-5701 fix(conversion): compare the conversion marker by identity, not equals#1874
lukaszlenart merged 2 commits into
mainfrom
fix/WW-5701-collection-converter-identity

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes WW-5701

Problem

CollectionConverter decided whether an element had converted successfully by
comparing the result to TypeConverter.NO_CONVERSION_POSSIBLE with equals().
The marker's value is the ordinary text ognl.NoConversionPossible, so an
element that genuinely held that text converted fine and was then silently
discarded.

vs.setValue("names", new String[]{"alpha", "ognl.NoConversionPossible", "omega"});

result:   [alpha, omega]
expected: [alpha, ognl.NoConversionPossible, omega]

Nothing signalled the loss. No conversion had failed, so no conversion error was
registered — the action simply saw a shorter collection.

The exposure is wider than collections declared to hold String: at
CollectionConverter.java:48-50, when no element type can be determined the
member type defaults to String.class, so untyped collections are affected too.

Fix

Compare by reference instead, at all three sites (lines 64, 75, 83).

Please do not simplify this back to equals() — that is what caused the bug.
Identity is correct here rather than incidental:

  • The constant is declared Object, not String
    (TypeConverter.java:49), so it is not a JLS constant variable and is not
    inlined into referencing class files. Every reference resolves to the one field
    value at runtime, third-party converters compiled elsewhere included.
  • A parameter value built by a servlet container from request bytes is a distinct
    object, so reference comparison separates "the converter signalled failure"
    from "the user submitted this text".

Scope, stated precisely

This protects values arriving from a request, which is the case that matters.
It does not protect a value that happens to be interned — application code
calling the converter programmatically with a String literal would still see it
dropped, because all identical literals share one interned instance.

Closing that too would mean giving the marker an identity no user string can
share, which changes a published constant and is a binary-compatibility question
rather than a bug fix. Out of scope here, and noted so the limit is not
misread as an oversight.

Testing

New CollectionConverterTest, written test-first and mutation-checked: the
production change was stashed to confirm the test fails without it
([alpha, omega]).

  • testElementWhoseTextEqualsTheMarkerIsKept — the fixture is built at runtime
    rather than written as a literal, precisely because a literal would be interned
    to the same instance as the constant and would not represent a real request.
    An assertNotSame guards that, so the test cannot silently regress into
    testing nothing.
  • testUnconvertibleElementIsStillDropped — the guard must keep doing its job;
    this passed before and after the change.

Full core suite: 3199 tests, 0 failures.

Related

WW-5700 / #1873 fixes the
mirror-image defect in the map and list property accessors, which stored the
marker instead of skipping it, and uses identity comparison for the same reason.
This one was found while reviewing that fix. The two are independent and can
merge in either order.

🤖 Generated with Claude Code

…ot equals

CollectionConverter decided whether an element had converted successfully
by comparing the result to TypeConverter.NO_CONVERSION_POSSIBLE with
equals(). The marker's value is the ordinary text "ognl.NoConversionPossible",
so an element that genuinely held that text converted fine and was then
silently discarded from the resulting collection.

Nothing signalled the loss: no conversion had failed, so no conversion
error was registered and the action simply saw a shorter collection.
The exposure is not limited to collections declared to hold Strings -
when no element type can be determined the member type defaults to
String.class, so untyped collections are affected too.

Compare by reference instead, at all three sites.

Identity is correct here rather than incidental. The constant is declared
Object, not String, so it is not a JLS constant variable and is not
inlined into referencing class files; every reference resolves to the one
field value at runtime, including in third-party converters compiled
elsewhere. A parameter value built by a servlet container from request
bytes is a distinct object, so reference comparison separates "the
converter signalled failure" from "the user submitted this text". Please
do not simplify this back to equals(), which is what caused the bug.

WW-5700 fixed the mirror-image defect in the map and list property
accessors, which stored the marker instead of skipping it, and used
identity comparison for the same reason. Found while reviewing that fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e guard paths

The Sonar quality gate on the pull request failed at 77.8% coverage of new
code: the marker guard was only exercised on the array-source path, leaving
the false branch of the other two guards uncovered.

Both added paths are reachable from a request - a Set-typed property fed from
a List, and a single-valued parameter assigned to a collection property. The
single-value holder is seeded before the assignment so that a setter which is
never called cannot make the test pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes WW-5701 by preventing CollectionConverter from discarding legitimate user-supplied values that happen to equal the textual representation of the NO_CONVERSION_POSSIBLE marker.

Changes:

  • Switch marker checks in CollectionConverter from equals() to reference comparison (!=) to correctly detect converter-failure signalling.
  • Add a new CollectionConverterTest covering the WW-5701 regression and related conversion-drop scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
core/src/main/java/org/apache/struts2/conversion/impl/CollectionConverter.java Uses identity comparison against NO_CONVERSION_POSSIBLE to avoid false positives when user values equal the marker’s text.
core/src/test/java/org/apache/struts2/conversion/impl/CollectionConverterTest.java Adds regression tests ensuring marker-text values are preserved and truly unconvertible values are still dropped.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

for (Object anObjArray : objArray) {
Object convertedValue = converter.convertValue(context, target, member, propertyName, anObjArray, memberType);
if (!NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != NO_CONVERSION_POSSIBLE) {
for (Object aCol : col) {
Object convertedValue = converter.convertValue(context, target, member, propertyName, aCol, memberType);
if (!NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != NO_CONVERSION_POSSIBLE) {
TypeConverter converter = getTypeConverter(context);
Object convertedValue = converter.convertValue(context, target, member, propertyName, value, memberType);
if (!NO_CONVERSION_POSSIBLE.equals(convertedValue)) {
if (convertedValue != NO_CONVERSION_POSSIBLE) {
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 655310b into main Aug 31, 2026
13 checks passed
@lukaszlenart
lukaszlenart deleted the fix/WW-5701-collection-converter-identity branch August 31, 2026 18:03
lukaszlenart added a commit that referenced this pull request Aug 31, 2026
…ckport) (#1879)

* WW-5701 fix(conversion): compare the conversion marker by identity, not equals

Backport of the 7.4.0 fix (#1874) to the 6.x line.

NO_CONVERSION_POSSIBLE is an ordinary String constant, so comparing with
equals() also matched a genuinely converted element whose own text happens
to be "ognl.NoConversionPossible" - and silently dropped it from the
collection. Only the constant instance itself signals a failed conversion,
so compare by identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* WW-5701 test(conversion): cover the collection-source and single-value guard paths

Backport of the coverage tests added on main, where the Sonar quality gate
failed at 77.8% coverage of new code: the marker guard was only exercised on
the array-source path, leaving the false branch of the other two guards
uncovered.

Both added paths are reachable from a request - a Set-typed property fed from
a List, and a single-valued parameter assigned to a collection property. The
single-value holder is seeded before the assignment so that a setter which is
never called cannot make the test pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants