Skip to content

Commit 7b434d8

Browse files
authored
Backport #479 fix to 2.21 (#599)
1 parent 70d6f4b commit 7b434d8

3 files changed

Lines changed: 200 additions & 2 deletions

File tree

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,14 +1206,18 @@ protected boolean _needsQuotingStrict(String value)
12061206
final int escLen = escCodes.length;
12071207
// 23-Sep-2020, tatu: [dataformats-text#217] Must also ensure line separator
12081208
// leads to quoting
1209+
// 17-Dec-2025, tatu: [dataformats-text#479] Must check for ALL line separators
1210+
// (LF, CR) per RFC 4180, not just the configured schema line separator.
12091211
final int lfFirst = (_cfgLineSeparatorLength == 0) ? 0 : _cfgLineSeparator[0];
12101212

12111213
for (int i = 0, len = value.length(); i < len; ++i) {
12121214
int c = value.charAt(i);
12131215
if (c < minSafe) {
12141216
if (c == _cfgColumnSeparator || c == _cfgQuoteCharacter
12151217
|| (c < escLen && escCodes[c] != 0)
1216-
|| (c == lfFirst)) {
1218+
|| (c == lfFirst)
1219+
// Per RFC 4180: must quote if contains LF or CR
1220+
|| (c == '\n') || (c == '\r')) {
12171221
return true;
12181222
}
12191223
}
@@ -1231,14 +1235,18 @@ protected boolean _needsQuotingStrict(String value, int esc)
12311235
final int escLen = escCodes.length;
12321236
// 23-Sep-2020, tatu: [dataformats-text#217] Must also ensure line separator
12331237
// leads to quoting
1238+
// 17-Dec-2025, tatu: [dataformats-text#479] Must check for ALL line separators
1239+
// (LF, CR) per RFC 4180, not just the configured schema line separator.
12341240
final int lfFirst = (_cfgLineSeparatorLength == 0) ? 0 : _cfgLineSeparator[0];
12351241

12361242
for (int i = 0, len = value.length(); i < len; ++i) {
12371243
int c = value.charAt(i);
12381244
if (c < minSafe) {
12391245
if (c == _cfgColumnSeparator || c == _cfgQuoteCharacter
12401246
|| (c < escLen && escCodes[c] != 0)
1241-
|| (c == lfFirst)) {
1247+
|| (c == lfFirst)
1248+
// Per RFC 4180: must quote if contains LF or CR
1249+
|| (c == '\n') || (c == '\r')) {
12421250
return true;
12431251
}
12441252
} else if (c == esc) {
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package com.fasterxml.jackson.dataformat.csv.ser;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.dataformat.csv.*;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
/**
10+
* Test for issue #479: STRICT_CHECK_FOR_QUOTING should properly quote
11+
* values containing newline characters per RFC 4180.
12+
* <p>
13+
* According to RFC 4180, fields containing special characters (including
14+
* newlines, carriage returns) MUST be enclosed in double quotes.
15+
* When STRICT_CHECK_FOR_QUOTING is enabled, this requirement should
16+
* still be met.
17+
*/
18+
public class StrictQuotingNewline479Test extends ModuleTestBase
19+
{
20+
private final CsvMapper MAPPER = mapperForCsv();
21+
22+
/**
23+
* Test that values with Unix line separator (\n) are properly quoted
24+
* when STRICT_CHECK_FOR_QUOTING is enabled.
25+
*/
26+
@Test
27+
public void testStrictQuotingWithUnixLineSeparator() throws Exception
28+
{
29+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
30+
.withLineSeparator("\n");
31+
32+
final IdDesc value = new IdDesc("line1\nline2", "description");
33+
34+
// With STRICT_CHECK_FOR_QUOTING enabled, newlines should still be quoted
35+
String csv = MAPPER.writer(schema)
36+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
37+
.writeValueAsString(value);
38+
39+
// Expected: value with newline should be quoted per RFC 4180
40+
assertEquals("\"line1\nline2\",description\n", csv);
41+
}
42+
43+
/**
44+
* Test that values with Windows line separator (\r\n) are properly quoted
45+
* when STRICT_CHECK_FOR_QUOTING is enabled.
46+
*/
47+
@Test
48+
public void testStrictQuotingWithWindowsLineSeparator() throws Exception
49+
{
50+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
51+
.withLineSeparator("\n");
52+
53+
final IdDesc value = new IdDesc("line1\r\nline2", "description");
54+
55+
// With STRICT_CHECK_FOR_QUOTING enabled, CR+LF should still be quoted
56+
String csv = MAPPER.writer(schema)
57+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
58+
.writeValueAsString(value);
59+
60+
// Expected: value with CRLF should be quoted per RFC 4180
61+
assertEquals("\"line1\r\nline2\",description\n", csv);
62+
}
63+
64+
/**
65+
* Test that values with Mac line separator (\r) are properly quoted
66+
* when STRICT_CHECK_FOR_QUOTING is enabled.
67+
*/
68+
@Test
69+
public void testStrictQuotingWithMacLineSeparator() throws Exception
70+
{
71+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
72+
.withLineSeparator("\n");
73+
74+
final IdDesc value = new IdDesc("line1\rline2", "description");
75+
76+
// With STRICT_CHECK_FOR_QUOTING enabled, CR should still be quoted
77+
String csv = MAPPER.writer(schema)
78+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
79+
.writeValueAsString(value);
80+
81+
// Expected: value with CR should be quoted per RFC 4180
82+
assertEquals("\"line1\rline2\",description\n", csv);
83+
}
84+
85+
/**
86+
* Test that values with multiple newlines are properly quoted
87+
* when STRICT_CHECK_FOR_QUOTING is enabled.
88+
*/
89+
@Test
90+
public void testStrictQuotingWithMultipleNewlines() throws Exception
91+
{
92+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
93+
.withLineSeparator("\n");
94+
95+
final IdDesc value = new IdDesc("line1\nline2\nline3", "description");
96+
97+
// With STRICT_CHECK_FOR_QUOTING enabled, multiple newlines should be quoted
98+
String csv = MAPPER.writer(schema)
99+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
100+
.writeValueAsString(value);
101+
102+
// Expected: value with multiple newlines should be quoted
103+
assertEquals("\"line1\nline2\nline3\",description\n", csv);
104+
}
105+
106+
/**
107+
* Verify that comma-containing values ARE properly quoted with
108+
* STRICT_CHECK_FOR_QUOTING (this should work as per the issue report).
109+
*/
110+
@Test
111+
public void testStrictQuotingWithCommaWorks() throws Exception
112+
{
113+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
114+
.withLineSeparator("\n");
115+
116+
final IdDesc value = new IdDesc("line1,line2", "description");
117+
118+
// With STRICT_CHECK_FOR_QUOTING enabled, commas should be quoted
119+
String csv = MAPPER.writer(schema)
120+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
121+
.writeValueAsString(value);
122+
123+
// Expected: value with comma should be quoted (this works according to issue)
124+
assertEquals("\"line1,line2\",description\n", csv);
125+
}
126+
127+
/**
128+
* Test newline in second column to ensure the bug affects any column position.
129+
*/
130+
@Test
131+
public void testStrictQuotingNewlineInSecondColumn() throws Exception
132+
{
133+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
134+
.withLineSeparator("\n");
135+
136+
final IdDesc value = new IdDesc("id123", "desc\nwith\nnewlines");
137+
138+
// With STRICT_CHECK_FOR_QUOTING enabled, newlines in any column should be quoted
139+
String csv = MAPPER.writer(schema)
140+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
141+
.writeValueAsString(value);
142+
143+
// Expected: second column with newlines should be quoted
144+
assertEquals("id123,\"desc\nwith\nnewlines\"\n", csv);
145+
}
146+
147+
/**
148+
* Test with custom line separator in schema (Windows-style).
149+
*/
150+
@Test
151+
public void testStrictQuotingWithCustomLineSeparatorCRLF() throws Exception
152+
{
153+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
154+
.withLineSeparator("\r\n");
155+
156+
final IdDesc value = new IdDesc("line1\nline2", "description");
157+
158+
// Even with different schema line separator, embedded newlines should be quoted
159+
String csv = MAPPER.writer(schema)
160+
.with(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
161+
.writeValueAsString(value);
162+
163+
// Expected: embedded newline should be quoted
164+
assertEquals("\"line1\nline2\",description\r\n", csv);
165+
}
166+
167+
/**
168+
* Comparison test: verify that WITHOUT STRICT_CHECK_FOR_QUOTING,
169+
* newlines ARE properly quoted (as mentioned in the issue).
170+
*/
171+
@Test
172+
public void testQuotingWithoutStrictCheckWorks() throws Exception
173+
{
174+
final CsvSchema schema = MAPPER.schemaFor(IdDesc.class)
175+
.withLineSeparator("\n");
176+
177+
final IdDesc value = new IdDesc("line1\nline2", "description");
178+
179+
// WITHOUT STRICT_CHECK_FOR_QUOTING (default loose mode), should be quoted
180+
String csv = MAPPER.writer(schema)
181+
.without(CsvGenerator.Feature.STRICT_CHECK_FOR_QUOTING)
182+
.writeValueAsString(value);
183+
184+
// This should work correctly per the issue report
185+
assertEquals("\"line1\nline2\",description\n", csv);
186+
}
187+
}

release-notes/VERSION-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Active Maintainers:
2323
when binding to Object type (POJO etc)
2424
(reported by Sergio D)
2525
(fix by @cowtowncoder, w/ Claude code)
26+
#479: (csv) `STRICT_CHECK_FOR_QUOTING` does not quote value that contains newline character
27+
(reported by @zhuhw)
28+
(fix by @cowtowncoder, w/ Claude code)
2629

2730
2.20.1 (30-Oct-2025)
2831

0 commit comments

Comments
 (0)