Motivation & Context
In recent community discussions, issues, and PRs concerning Converter read/write logic, the intention behind supportExcelTypeKey() has been a recurring point of confusion:
"Why is supportExcelTypeKey() return null frequently required in write mode? When does it take effect, and is this considered standard practice?"
Root Cause Analysis
Tracing the evolution from EasyExcel to FastExcel, this confusion stems from historical technical debt accumulated as the framework expanded to support multiple output formats:
-
EasyExcel Era:
Originally, write converter registration was bound strictly to supportJavaTypeKey, agnostic of the target Excel cell type (since the output format is determined dynamically by the returned WriteCellData). When CSV export support was introduced, AbstractExcelWriteExecutor#doConvert hardcoded if (excelType == CSV) { setTargetCellDataType(STRING); }. This coupled the write pipeline with supportExcelTypeKey for the first time.
-
FastExcel Era:
As features evolved—such as supporting differential conversions for identical Java types—write-mode registration also began strictly binding supportExcelTypeKey, influenced by the inertia of the earlier CSV hard-coupling. This introduced an unexpected side effect: standard write converters began throwing UnsupportedOperationException during registration. To bypass this exception and keep exports functioning, developers resorted to supportExcelTypeKey() { return null; } as a workaround.
-
Current State:
Over time, returning null as a workaround was widely copied throughout the ecosystem. This obscured the fundamental asymmetry between read and write models and created substantial confusion regarding the true purpose of supportExcelTypeKey in write scenarios.
Fundamentally, supportExcelTypeKey() holds physical meaning exclusively in read mode, where it serves as an invariant prerequisite and must be explicitly specified (non-null).
Core Goals
- Clarify that
supportExcelTypeKey belongs solely to the read model; remove all implicit dependencies and workarounds from write workflows.
- Introduce
WriteConverter and ReadConverter interfaces, clarifying boundaries and reducing cognitive overhead for custom converters.
- Eliminate hardcoded CSV type-binding hacks in the executor, making a single write converter naturally compatible across
XLSX, XLS, and CSV.
- Introduce
ColumnBinding to address scenarios where identical Java types require distinct formatting across different columns in no-bean modes.
- Introduce
CellDataConverterRegistry to manage hierarchical lifecycle and caching, replacing the scattered raw converterMap.
- Ensure compatibility with the existing
Converter<T> interface, the converter registration entry point registerConverter(...) in the public *Builder, and the use of @ExcelProperty(converter = ...).
Proposed Design
Decouple WriteConverter and ReadConverter
Segregate the monolithic Converter into role-specific interfaces:
WriteConverter<T>:
supportJavaTypeKey()
convertToExcelData(T, ExcelContentProperty, GlobalConfiguration)
convertToExcelData(WriteConverterContext<T>)
ReadConverter<T> :
supportJavaTypeKey()
supportExcelTypeKey() (non-null contract)
convertToJavaData(ReadCellData<?>, ExcelContentProperty, GlobalConfiguration)
convertToJavaData(ReadConverterContext<?>)
- Aggregate Compatibility Interface (
Converter<T>):
Inherits both interfaces (public interface Converter<T> extends ReadConverter<T>, WriteConverter<T>), ensuring existing code compiles and runs without modification.
Column-Scoped Capability Interface (ColumnBinding)
For no-bean modes (List<List<Object>>) where identical Java types require different formatting across columns (e.g., Column 1: Boolean -> "True/False", Columns 2–3: Boolean -> "Yes/No"):
// Binds strictly to Column 1
public class BooleanString1Converter implements WriteConverter<Boolean>, ColumnBinding {
@Override
public Class<?> supportJavaTypeKey() {
return Boolean.class;
}
@Override
public Integer columnIndex() {
return 1;
}
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Boolean> context) {
return new WriteCellData<>(context.getValue().toString());
}
}
// Binds to Columns 2 and 3
public class BooleanString2Converter implements WriteConverter<Boolean>, ColumnBinding {
@Override
public Class<?> supportJavaTypeKey() {
return Boolean.class;
}
@Override
public Set<Integer> columnIndexes() {
return new HashSet<>(Arrays.asList(2, 3));
}
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Boolean> context) {
return new WriteCellData<>(context.getValue() ? "Yes" : "No");
}
}
Extensibility Note: ColumnBinding specifically targets the common requirement of column-level scoping. If other matching dimensions are needed in the future, a generalized parent interface (such as ConditionalConverter) can be introduced seamlessly, with ColumnBinding acting as a specialized sub-interface.
Unified Converter Registry (CellDataConverterRegistry)
Replaces the Map<ConverterKeyBuild.ConverterKey, Converter<?>> converterMap inside AbstractHolder with a unified registry, CellDataConverterRegistry, providing dedicated registration and lookup methods.
- Registration Precedence: When types or matching conditions are identical, later-registered converters take precedence over earlier ones (Later-registered > Earlier-registered / Last-in Wins).
- Resolution Precedence: Custom (Write/Read)Converters + ColumnBinding > Custom (Write/Read)Converters > Framework default Converters.
Eliminate Executor Hardcoding
Clean up DefaultConverterLoader and eliminate the CSV-specific type-override branch in AbstractExcelWriteExecutor#doConvert:
// After refactoring: Clean, unified resolution
WriteConverter<?> writeConverter = writeContext.currentWriteHolder()
.getConverterRegistry()
.findWriteConverter(cellWriteHandlerContext.getOriginalFieldClass(), cellWriteHandlerContext.getColumnIndex());
API Evolution & Registration Compatibility
Provide dedicated registration methods on builders while preserving backward compatibility:
// Dedicated registration APIs
public T registerWriteConverter(WriteConverter<?> converter) { ... }
public T registerReadConverter(ReadConverter<?> converter) { ... }
// Legacy registration API (backward-compatible)
@Override
public T registerConverter(Converter<?> converter) {
// Automatically routes internally to the corresponding read/write registration flow
}
Compatibility & Breaking Changes
Public API Compatibility
- Existing implementations of
Converter<T> behavior remains unchanged.
- Existing implementations of
@ExcelProperty(converter = ...) behavior remains unchanged.
- Existing implementations of
registerConverter(...) behavior remains unchanged.
Internal SPI Breaking Changes
- Internal State Encapsulation:
- The raw
converterMap previously exposed in internal contexts (e.g., ConfigurationHolder#converterMap and ConverterUtils#convertToJavaType) is encapsulated within CellDataConverterRegistry.
- Plugins that directly accessed or mutated
converterMap via reflection must migrate to the public registry APIs.
- Preloaded Default Collections:
- Redundant
String write converter mappings preloaded specifically for CSV in DefaultConverterLoader have been cleaned up and separated into dedicated sets.
Are you willing to submit a PR?
Motivation & Context
In recent community discussions, issues, and PRs concerning Converter read/write logic, the intention behind
supportExcelTypeKey()has been a recurring point of confusion:Root Cause Analysis
Tracing the evolution from EasyExcel to FastExcel, this confusion stems from historical technical debt accumulated as the framework expanded to support multiple output formats:
EasyExcel Era:
Originally, write converter registration was bound strictly to
supportJavaTypeKey, agnostic of the target Excel cell type (since the output format is determined dynamically by the returnedWriteCellData). When CSV export support was introduced,AbstractExcelWriteExecutor#doConverthardcodedif (excelType == CSV) { setTargetCellDataType(STRING); }. This coupled the write pipeline withsupportExcelTypeKeyfor the first time.FastExcel Era:
As features evolved—such as supporting differential conversions for identical Java types—write-mode registration also began strictly binding
supportExcelTypeKey, influenced by the inertia of the earlier CSV hard-coupling. This introduced an unexpected side effect: standard write converters began throwingUnsupportedOperationExceptionduring registration. To bypass this exception and keep exports functioning, developers resorted tosupportExcelTypeKey() { return null; }as a workaround.Current State:
Over time, returning
nullas a workaround was widely copied throughout the ecosystem. This obscured the fundamental asymmetry between read and write models and created substantial confusion regarding the true purpose ofsupportExcelTypeKeyin write scenarios.Fundamentally,
supportExcelTypeKey()holds physical meaning exclusively in read mode, where it serves as an invariant prerequisite and must be explicitly specified (non-null).Core Goals
supportExcelTypeKeybelongs solely to the read model; remove all implicit dependencies and workarounds from write workflows.WriteConverterandReadConverterinterfaces, clarifying boundaries and reducing cognitive overhead for custom converters.XLSX,XLS, andCSV.ColumnBindingto address scenarios where identical Java types require distinct formatting across different columns in no-bean modes.CellDataConverterRegistryto manage hierarchical lifecycle and caching, replacing the scattered rawconverterMap.Converter<T>interface, the converter registration entry pointregisterConverter(...)in the public*Builder, and the use of@ExcelProperty(converter = ...).Proposed Design
Decouple
WriteConverterandReadConverterSegregate the monolithic
Converterinto role-specific interfaces:WriteConverter<T>:supportJavaTypeKey()convertToExcelData(T, ExcelContentProperty, GlobalConfiguration)convertToExcelData(WriteConverterContext<T>)ReadConverter<T>:supportJavaTypeKey()supportExcelTypeKey()(non-null contract)convertToJavaData(ReadCellData<?>, ExcelContentProperty, GlobalConfiguration)convertToJavaData(ReadConverterContext<?>)Converter<T>):Inherits both interfaces (
public interface Converter<T> extends ReadConverter<T>, WriteConverter<T>), ensuring existing code compiles and runs without modification.Column-Scoped Capability Interface (
ColumnBinding)For no-bean modes (
List<List<Object>>) where identical Java types require different formatting across columns (e.g., Column 1:Boolean -> "True/False", Columns 2–3:Boolean -> "Yes/No"):Unified Converter Registry (
CellDataConverterRegistry)Replaces the
Map<ConverterKeyBuild.ConverterKey, Converter<?>> converterMapinsideAbstractHolderwith a unified registry,CellDataConverterRegistry, providing dedicated registration and lookup methods.Eliminate Executor Hardcoding
Clean up
DefaultConverterLoaderand eliminate the CSV-specific type-override branch inAbstractExcelWriteExecutor#doConvert:API Evolution & Registration Compatibility
Provide dedicated registration methods on builders while preserving backward compatibility:
Compatibility & Breaking Changes
Public API Compatibility
Converter<T>behavior remains unchanged.@ExcelProperty(converter = ...)behavior remains unchanged.registerConverter(...)behavior remains unchanged.Internal SPI Breaking Changes
converterMappreviously exposed in internal contexts (e.g.,ConfigurationHolder#converterMapandConverterUtils#convertToJavaType) is encapsulated withinCellDataConverterRegistry.converterMapvia reflection must migrate to the public registry APIs.Stringwrite converter mappings preloaded specifically for CSV inDefaultConverterLoaderhave been cleaned up and separated into dedicated sets.Are you willing to submit a PR?