Skip to content

[Bug]POJO Export with custom converter StackOverflow Repro #889

@fyeeme

Description

@fyeeme

Search before asking

  • I searched in the 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:
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.
 *
 * <p>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.</p>
 */
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<String> {
        @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<ZonedDateTime> {
        @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

@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

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:

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:

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:

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:

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions