Skip to content

feat: honor wildcard custom converters when reading - #1086

Open
BigDataDZ wants to merge 4 commits into
apache:mainfrom
BigDataDZ:fix/read-wildcard-converter
Open

feat: honor wildcard custom converters when reading#1086
BigDataDZ wants to merge 4 commits into
apache:mainfrom
BigDataDZ:fix/read-wildcard-converter

Conversation

@BigDataDZ

Copy link
Copy Markdown

Purpose of the pull request

Custom converters registered with supportExcelTypeKey() == null (the wildcard contract: "matches
every cell data type") never applied on the read path: the built-in concrete-key converter silently
took over, or reading failed with Converter not found. With this change, a single
registerConverter(...) call works for reading exactly as it already does for writing.

Closed: #1085

What's changed?

AbstractReadHolder now routes custom converter registration through registerCustomConverter,
which — when supportExcelTypeKey() is null — additionally registers the converter under every
concrete CellDataTypeEnum key (all except EMPTY, which is filtered before lookup).

Read lookups (ConverterUtils#convertToJavaObject, ConverterUtils#convertToStringMap) use the
concrete cell type as key, so a wildcard registration at (JavaType, null) never matched: e.g. a
wildcard Boolean converter reading a "yes" cell produced false via the built-in
BooleanStringConverter (Boolean.valueOf("yes")), with no warning. This is the read-path twin of
#1045/#1056 (write/CSV flavor, addressed by PR #1069) and mirrors that PR's registration-expansion
approach, expanded to all concrete types because read lookups can arrive with any cell type.

Lookup code is untouched. Explicit-key registrations keep their priority semantics, and users
without wildcard custom converters are unaffected.

New WildcardConverterReadTest (round-trip): the wildcard-read case fails on current main
("yes" read as false) and passes with this change; the explicit STRING-key case is a
regression guard. Full fesod-sheet suite: 920/920 green.

Checklist

  • I have read the Contributor Guide.
  • I have written the necessary doc or comment.
  • I have added the necessary unit tests and all cases have passed.

@BigDataDZ BigDataDZ changed the title fix: honor wildcard custom converters when reading feat: honor wildcard custom converters when reading Sep 10, 2026
Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com>

@skytin1004 skytin1004 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.

Hi @BigDataDZ,
I tried registering a STRING converter first and a wildcard converter second. Before this change, the STRING converter handled string cells. With this change, the wildcard converter handles them instead.
Is this change intentional? If not, could you keep the STRING converter handling these cells and add a test for this registration order?

Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com>
@BigDataDZ

Copy link
Copy Markdown
Author

Good catch — that behavior change was not intentional. Fixed in d318eea:

Custom registrations now happen in two phases — first every converter is registered at its exact
(JavaType, CellDataType) key, then wildcard registrations are expanded to the concrete keys
skipping already-claimed ones. Explicit registrations therefore keep priority over wildcard ones
regardless of registration order, while wildcard expansion still overrides built-in defaults
(loaded before any custom converter), so the fix for #1085 is unaffected.

Added the suggested registration-order test (testExplicitStringKeyConverterWinsOverLaterWildcardRegistration):
explicit STRING first + wildcard second now keeps the STRING converter handling string cells —
red before this commit, green after. Full fesod-sheet suite green.

@skytin1004 skytin1004 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.

@BigDataDZ Thanks for addressing this. I verified that the explicit converter keeps precedence over a later wildcard registration, while the wildcard case still works. LGTM.

@Mikkey-f Mikkey-f 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.

I went through the read-side lookup path to compare it with the write-side change in #1069, and the mechanism looks right. A few notes below, one of which I think is worth covering before merge.

Verified

  • No leak risk: AbstractReadHolder builds a per-instance HashMap, and the expansion only writes to that copy — the shared default map is untouched.
  • The explicitKeys first pass makes the "explicit wins regardless of registration order" guarantee hold (ConverterKey implements equals/hashCode, so contains() is reliable).
  • Skipping EMPTY is safe: both lookup sites (convertToStringMap, doConvertToJavaObject) short-circuit EMPTY before reaching the map.
  • In the expanded set, ERROR is genuinely reachable (in v03, FormulaRecordHandler leaves error cells as ERROR; only later formats normalise them to STRING), so the expansion is not dead code there. RICH_TEXT_STRING does not appear to be produced on the read side — harmless, just unused.

Why this differs from #1069 (write side)

Write lookups key on (javaType, targetCellDataType), where xlsx targets are null (so the wildcard key matches directly) and only the CSV / fill paths force a concrete type — hence STRING is enough there. Read lookups key on the actual cell type, so honouring a wildcard means expanding to every concrete type. A one-line comment near the expansion loop would help keep this asymmetry from looking like a bug to the next reader.

The case I would suggest addressing: the expansion also reaches the header path

ConverterUtils.convertToStringMap (used by DefaultAnalysisEventProcessor.buildHead for header-to-field matching and by AnalysisEventListener.invokeHeadMap) looks up (String.class, cellDataType). Because the expansion writes (String, STRING) and friends for a wildcard Converter<String>, the converter now runs on header cells as well.

Local check — bean with @ExcelProperty("flag"), file with header flag and cell abc, converter that upper-cases a wildcard-registered String:

  • on main: the converter is never invoked and the value reads back as abc (today's no-op);
  • with this change: it is invoked with "flag" (the header), the header text becomes "FLAG", name matching finds no column, and the field comes back null with no exception. With @ExcelProperty(index = 0) it still works, since matching then ignores the header text.

So for a wildcard String converter this turns a no-op into silently empty fields. The tests here do not catch it because they register a Boolean converter while the header lookup is for String.class.

To be clear, this is not introduced by your change — an explicit (String, STRING) registration already behaves this way on main (I checked: same silent null). But the PR widens the set of registrations that hit it, and a generic string converter is exactly the kind of thing people register wildcard-style.

I filed #1098 with the repro and root cause, and I am happy to take that fix so this PR can stay focused on the wildcard expansion — my suggestion there is that the header path should resolve against the built-in converters rather than the user-extended map. If you would rather handle it here, just say so and I will close the issue and follow your lead; either way, a test with a wildcard String converter would be worth adding on your side.

Worth knowing: until that header fix lands, a wildcard Converter<String> would give silently empty fields for name-based matching, so if you or the maintainers prefer to land the header fix first, I will move quickly on it.

Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com>
@BigDataDZ

Copy link
Copy Markdown
Author

Thanks for the incredibly thorough review @Mikkey-f — the leak/ordering/EMPTY/ERROR walk-through
is exactly the kind of scrutiny this change needed.

  1. Asymmetry note added next to the expansion loop (8dd46da), spelling out why the read side
    expands to every concrete type while fix: honor wildcard-key custom converters when writing CSV (#1045) (#1056) #1069 only needed STRING on the write side.
  2. Wildcard Converter<String> test added (testWildcardStringConverterAppliesToDataCells,
    index-based matching so the header path stays out of scope here): the converter applies to
    data cells as expected.
  3. Agreed on [Bug] Header cells are converted with user-registered String converters, breaking @ExcelProperty(name) matching #1098 — the header path resolving against user-extended converters is a
    pre-existing flaw that this PR merely widens, and it deserves its own focused fix. It's all
    yours, much appreciated!

One sequencing note for maintainers: until #1098 lands, a wildcard Converter<String> remains a
hazard for name-based matching (as your analysis shows), so the merge order between this PR and
#1098 may be worth considering.

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.

[Enhancement] Custom converters registered with supportExcelTypeKey() == null never apply when reading

4 participants