apache / apache/fesod

[Bug]POJO Export with custom converter StackOverflow Repro

Open
#889 3 comments 0 reactions 0 assignees View on GitHub
bug waiting for feedback
Dominant language
Java
Stars
6.2k
Forks
532
Avg merge
1d 3h
Merged PRs (30d)
42

Description

### Search before asking

- [x] I searched in the [issues](https://github.com/apache/fesod/issues) and found nothing similar.

### Fesod version

2.0.1-incubating

### JDK version

21

### Operating system

macos 26.4

### Steps To Reproduce

## Summary

Using the native POJO write path in `org.apache.fesod:fesod-sheet:2.0.1-incubating`
can trigger a `StackOverflowError` when:

- the DTO contains a `ZonedDateTime` field
- a custom converter is registered for `ZonedDateTime`
- the DTO also uses normal field metadata such as:
- `@DateTimeFormat`
- `@NumberFormat`
- `@ExcelProperty(converter = ...)`

In our project this blocks switching back to the pure native Fesod POJO export path
for `ZonedDateTime`-based exports.

## Environment

- Java: project currently runs on Java 21
- Library: `org.apache.fesod:fesod-sheet:2.0.1-incubating`
- Output format: XLSX

## Minimal Reproduction

Repository repro artifact:

- Test file:

```java
package com.linzi.pitpat.excel;

import lombok.Data;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
import org.apache.fesod.sheet.annotation.format.NumberFormat;
import org.apache.fesod.sheet.converters.Converter;
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Documents an upstream Fesod bug we can reproduce reliably.
*
*

When using the native POJO write path with a DTO containing ZonedDateTime,
* Fesod 2.0.1-incubating triggers a StackOverflowError inside its write holder hashCode chain.
* We keep this test disabled because enabling it would break CI, but it provides a stable
* reproduction if we need to validate an upstream fix or a local dependency patch.


*/
class FesodNativeMetadataRegressionTest {

@Test
@Disabled("Stable repro for upstream Fesod StackOverflowError on native POJO write path")
@DisplayName("native Fesod POJO export should reproduce StackOverflowError with ZonedDateTime metadata")
void nativePojoExport_shouldReproduceStackOverflowError() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

assertThrows(StackOverflowError.class, () -> FesodSheet.write(outputStream, NativeMixedMetadataRow.class)
.registerConverter(new NativeZonedDateStringConverter())
.sheet("NativeMixedMetadata")
.doWrite(List.of(createRow())));
}

private NativeMixedMetadataRow createRow() {
NativeMixedMetadataRow row = new NativeMixedMetadataRow();
row.setStatus("ACTIVE");
row.setAmount(new BigDecimal("1234.5"));
row.setCreatedAt(ZonedDateTime.of(2026, 3, 24, 15, 30, 45, 0, ZoneId.of("Asia/Shanghai")));
return row;
}

@Data
public static class NativeMixedMetadataRow {
@ExcelProperty(value = "Status", converter = NativeStatusLabelConverter.class)
private String status;

@ExcelProperty("Amount")
@NumberFormat("¥#,##0.00")
private BigDecimal amount;

@ExcelProperty("Created At")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private ZonedDateTime createdAt;
}

public static class NativeStatusLabelConverter implements Converter {
@Override
public Class supportJavaTypeKey() {
return String.class;
}

@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}

@Override
public WriteCellData convertToExcelData(
String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return new WriteCellData<>("STATUS:" + value);
}
}

public static class NativeZonedDateStringConverter implements Converter {
@Override
public Class supportJavaTypeKey() {
return ZonedDateTime.class;
}

@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}

@Override
public WriteCellData convertToExcelData(
ZonedDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String format = contentProperty != null && contentProperty.getDateTimeFormatProperty() != null
? contentProperty.getDateTimeFormatProperty().getFormat()
: "yyyy-MM-dd HH:mm:ss";
return new WriteCellData<>(value.toLocalDateTime().format(java.time.format.DateTimeFormatter.ofPattern(format)));
}
}
}
```

The test is intentionally marked `@Disabled` so CI stays green, but it is a stable reproducer.

### Repro DTO

```java
@Data
public static class NativeMixedMetadataRow {
@ExcelProperty(value = "Status", converter = NativeStatusLabelConverter.class)
private String status;

@ExcelProperty("Amount")
@NumberFormat("¥#,##0.00")
private BigDecimal amount;

@ExcelProperty("Created At")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private ZonedDateTime createdAt;
}
```

### Repro Export Code

```java
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

FesodSheet.write(outputStream, NativeMixedMetadataRow.class)
.registerConverter(new NativeZonedDateStringConverter())
.sheet("NativeMixedMetadata")
.doWrite(List.of(row));
```

### Expected Result

Workbook is exported successfully, with native Fesod metadata handling preserved:

- `@ExcelProperty(converter = ...)` is applied
- `@NumberFormat` is applied
- `@DateTimeFormat` is applied

### Actual Result

`StackOverflowError`

## Observed Stack Pattern

The stack repeatedly cycles through `hashCode()` calls on Fesod write holder objects:

```text
java.lang.StackOverflowError
at java.util.ArrayList.hashCode(...)
at org.apache.fesod.sheet.metadata.Head.hashCode(...)
at org.apache.fesod.sheet.metadata.property.ExcelHeadProperty.hashCode(...)
at org.apache.fesod.sheet.write.property.ExcelWriteHeadProperty.hashCode(...)
at org.apache.fesod.sheet.write.metadata.holder.AbstractWriteHolder.hashCode(...)
at org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder.hashCode(...)
at org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder.hashCode(...)
...
```

## Suspected Root Cause

This is an inference based on source inspection plus stable reproduction.

`WriteContextImpl.initSheet()` stores `WriteSheetHolder` into maps owned by `WriteWorkbookHolder`:

```java
writeWorkbookHolder.getHasBeenInitializedSheetIndexMap().put(writeSheetHolder.getSheetNo(), writeSheetHolder);
writeWorkbookHolder.getHasBeenInitializedSheetNameMap().put(writeSheetHolder.getSheetName(), writeSheetHolder);
```

At the same time:

- `WriteSheetHolder` holds `parentWriteWorkbookHolder`
- `WriteWorkbookHolder` holds maps of `WriteSheetHolder`
- `AbstractWriteHolder`, `WriteWorkbookHolder`, and `WriteSheetHolder` use Lombok `@EqualsAndHashCode`

That appears to create a recursive object graph for structural `hashCode()` evaluation:

```text
WriteWorkbookHolder
-> hasBeenInitializedSheetIndexMap / hasBeenInitializedSheetNameMap
-> WriteSheetHolder
-> parentWriteWorkbookHolder
-> WriteWorkbookHolder
-> ...
```

If Fesod evaluates structural `hashCode()` on these holder objects, recursion does not terminate.

## Project Impact

In our project:

- the plain native POJO path would be the preferred implementation
- but this Fesod issue prevents using it safely for `ZonedDateTime`
- we currently use a compatibility path that still reuses Fesod field metadata instead of fully hardcoding formatting logic

## Suggested Fix Direction

Likely fix directions in Fesod:

- exclude recursive holder references from `equals/hashCode`
- or avoid structural `equals/hashCode` for runtime write holders entirely
- or replace the relevant holder equality semantics with identity-based handling

## Local Verification

Project-level verification after documenting the repro:

```bash
mvn -q -pl pitpat-common/pitpat-excel test
mvn -q -pl demo-service -am -DskipTests compile
```

### Current Behavior

StackOverflowError

### Expected Behavior

work normally

### Anything else?

_No response_

### Are you willing to submit a PR?

- [ ] I'm willing to submit a PR!

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.