Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions csv/src/main/java/tools/jackson/dataformat/csv/CsvParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ protected void _readHeaderLine() throws JacksonException {
final boolean failOnDupHeaders = CsvReadFeature.FAIL_ON_DUPLICATE_HEADER_COLUMNS.enabledIn(_formatFeatures);
final Set<String> seenNames = failOnDupHeaders ? new HashSet<>() : null;

// [dataformats-text#657]: Optionally match header names case-insensitively
final boolean caseInsensitive = CsvReadFeature.CASE_INSENSITIVE_HEADERS.enabledIn(_formatFeatures);

final boolean trimHeaderNames = CsvReadFeature.TRIM_HEADER_SPACES.enabledIn(_formatFeatures);
while ((name = _reader.nextString()) != null) {
// one more thing: always trim names, regardless of config settings
Expand All @@ -660,9 +663,12 @@ protected void _readHeaderLine() throws JacksonException {
"Duplicate header column \"%s\"", name));
}
// See if "old" schema defined type; if so, use that type...
CsvSchema.Column prev = _schema.column(name);
// [dataformats-text#657]: optionally use case-insensitive lookup
CsvSchema.Column prev = caseInsensitive
? _schema.columnIgnoreCase(name)
: _schema.column(name);
if (prev != null) {
builder.addColumn(name, prev.getType());
builder.addColumn(prev.getName(), prev.getType());
} else {
builder.addColumn(name);
}
Expand Down
17 changes: 17 additions & 0 deletions csv/src/main/java/tools/jackson/dataformat/csv/CsvReadFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ public enum CsvReadFeature
* @since 3.2
*/
FAIL_ON_DUPLICATE_HEADER_COLUMNS(true),

/**
* Feature that enables case-insensitive matching of header column names
* against schema column names. When enabled, a CSV header column named
* "TEMP_MAX" will match a schema column named "temp_max" (and vice versa).
*<p>
* This is useful when used together with
* {@code MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES} to allow
* case-insensitive header matching at the parser level, preventing
* {@link #FAIL_ON_MISSING_HEADER_COLUMNS} from incorrectly reporting
* columns as missing when they differ only by case.
*<p>
* Feature is disabled by default.
*
* @since 3.2
*/
CASE_INSENSITIVE_HEADERS(false),
;

private final boolean _defaultState;
Expand Down
23 changes: 23 additions & 0 deletions csv/src/main/java/tools/jackson/dataformat/csv/CsvSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,29 @@ public Column column(String name) {
return _columnsByName.get(name);
}

/**
* Case-insensitive variant of {@link #column(String)}: looks up a column
* by name, ignoring case differences.
*
* @param name Column name to look up (case-insensitive)
* @return Column with matching name, or {@code null} if not found
*
* @since 3.2
*/
public Column columnIgnoreCase(String name) {
// Try exact match first for efficiency
Column col = _columnsByName.get(name);
if (col != null) {
return col;
}
for (Column c : _columns) {
if (c.getName().equalsIgnoreCase(name)) {
return c;
}
}
return null;
}

/**
* Optimized variant where a hint is given as to likely index of the column
* name.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package tools.jackson.dataformat.csv.deser;

import java.util.Map;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.MappingIterator;

import tools.jackson.dataformat.csv.*;

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

/**
* Tests for [dataformats-text#657]: CSV header validation should support
* case-insensitive matching via {@link CsvReadFeature#CASE_INSENSITIVE_HEADERS}.
*/
public class CaseInsensitiveHeader657Test extends ModuleTestBase
{
private final CsvMapper MAPPER = mapperForCsv();

// [dataformats-text#657]: case-insensitive header should match schema columns
@Test
public void testCaseInsensitiveHeaderMatch() throws Exception
{
CsvSchema schema = CsvSchema.builder()
.setUseHeader(true)
.setReorderColumns(true)
.addColumn("name")
.addColumn("temp_max")
.build();

// Header has upper-case "TEMP_MAX" but schema defines "temp_max"
String CSV = "name,TEMP_MAX\nRoger,42\n";

MappingIterator<Map<String, Object>> it = MAPPER
.readerFor(Map.class)
.with(schema)
.with(CsvReadFeature.CASE_INSENSITIVE_HEADERS)
.readValues(CSV);
assertTrue(it.hasNext());
Map<?, ?> result = it.nextValue();
assertEquals("Roger", result.get("name"));
assertEquals("42", result.get("temp_max"));
}

// [dataformats-text#657]: without the feature, case mismatch should still fail
@Test
public void testCaseSensitiveHeaderStillFails() throws Exception
{
CsvSchema schema = CsvSchema.builder()
.setUseHeader(true)
.setReorderColumns(true)
.addColumn("name")
.addColumn("temp_max")
.build();

String CSV = "name,TEMP_MAX\nRoger,42\n";

try {
MappingIterator<Map<String, Object>> it = MAPPER
.readerFor(Map.class)
.with(schema)
.readValues(CSV);
it.nextValue();
fail("Should fail with missing column when case-insensitive is disabled");
} catch (CsvReadException e) {
verifyException(e, "Missing 1 header column: [\"temp_max\"]");
}
}

// [dataformats-text#657]: case-insensitive with reordered columns
@Test
public void testCaseInsensitiveReorderedColumns() throws Exception
{
CsvSchema schema = CsvSchema.builder()
.setUseHeader(true)
.setReorderColumns(true)
.addColumn("firstName")
.addColumn("lastName")
.addColumn("age")
.build();

// All upper-case and reordered
String CSV = "AGE,FIRSTNAME,LASTNAME\n25,John,Doe\n";

MappingIterator<Map<String, Object>> it = MAPPER
.readerFor(Map.class)
.with(schema)
.with(CsvReadFeature.CASE_INSENSITIVE_HEADERS)
.readValues(CSV);
assertTrue(it.hasNext());
Map<?, ?> result = it.nextValue();
assertEquals("John", result.get("firstName"));
assertEquals("Doe", result.get("lastName"));
assertEquals("25", result.get("age"));
}

// [dataformats-text#657]: case-insensitive should still detect truly missing columns
@Test
public void testCaseInsensitiveStillDetectsMissing() throws Exception
{
CsvSchema schema = CsvSchema.builder()
.setUseHeader(true)
.setReorderColumns(true)
.addColumn("name")
.addColumn("age")
.build();

// "agee" is a typo, not a case difference
String CSV = "name,agee\nRoger,18\n";

try {
MappingIterator<Map<String, Object>> it = MAPPER
.readerFor(Map.class)
.with(schema)
.with(CsvReadFeature.CASE_INSENSITIVE_HEADERS)
.readValues(CSV);
it.nextValue();
fail("Should fail: 'agee' is not a case-insensitive match for 'age'");
} catch (CsvReadException e) {
verifyException(e, "Missing 1 header column: [\"age\"]");
}
}
}
7 changes: 7 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,10 @@ Michael Carman (@mjcarman)
* Contributed #623: Add support for YAML 1.2 non-finite (Infinite, -Infinite, NaN)
numbers, Octal notation
(3.2.0)

Tomas Hugec (@polyoxidonium)
* Reported #657: (csv) CSV header validation ignores `ACCEPT_CASE_INSENSITIVE_PROPERTIES`:
add `CsvReadFeature.CASE_INSENSITIVE_HEADERS`
(3.2.0)


6 changes: 5 additions & 1 deletion release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ implementations)
#638: (properties) Support for `StreamReadConstraints.maxNameLength` and
`maxStringLength` for Properties parsing
(fix by @cowtowncoder, w/ Claude code)
#643: `CsvParser` doesn't handle whitespace outside of quoted values correctly
#643: (csv) `CsvParser` doesn't handle whitespace outside of quoted values correctly
(reported by @tomdz)
(fix by @cowtowncoder, w/ Claude code)
#657: (csv) CSV header validation ignores `ACCEPT_CASE_INSENSITIVE_PROPERTIES`:
add `CsvReadFeature.CASE_INSENSITIVE_HEADERS`
(reported by Tomas H)
(fix by @cowtowncoder, w/ Claude code)

3.1.1 (27-Mar-2026)

Expand Down