diff --git a/settings.gradle b/settings.gradle
index 52d38fb5800..51a38c2ea58 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -78,6 +78,7 @@ def subprojects = [
'groovy-test-junit5',
'groovy-test-junit6',
'groovy-testng',
+ 'groovy-csv',
'groovy-toml',
'groovy-typecheckers',
'groovy-xml',
diff --git a/subprojects/groovy-csv/build.gradle b/subprojects/groovy-csv/build.gradle
new file mode 100644
index 00000000000..57fdccd28a3
--- /dev/null
+++ b/subprojects/groovy-csv/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+plugins {
+ id 'org.apache.groovy-library'
+}
+
+dependencies {
+ api rootProject
+ implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-csv:${versions.jackson}"
+ implementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
+ testImplementation projects.groovyTest
+ testRuntimeOnly "com.fasterxml.jackson.core:jackson-annotations:${versions.jacksonAnnotations}"
+ testRuntimeOnly projects.groovyAnt // for JavadocAssertionTests
+}
+
+groovyLibrary {
+ optionalModule()
+ withoutBinaryCompatibilityChecks()
+}
diff --git a/subprojects/groovy-csv/src/main/java/groovy/csv/CsvBuilder.java b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvBuilder.java
new file mode 100644
index 00000000000..f11e67f2d55
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvBuilder.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.csv;
+
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import groovy.lang.Writable;
+import org.apache.groovy.lang.annotation.Incubating;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Builds CSV output from collections of maps or typed objects.
+ *
+ * Example with maps:
+ *
+ * def data = [[name: 'Alice', age: 30], [name: 'Bob', age: 25]]
+ * def csv = groovy.csv.CsvBuilder.toCsv(data)
+ * assert csv.contains('name,age')
+ * assert csv.contains('Alice,30')
+ *
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public class CsvBuilder implements Writable {
+ private final CsvMapper mapper;
+ private char separator = ',';
+ private char quoteChar = '"';
+ private String content;
+
+ public CsvBuilder() {
+ this.mapper = new CsvMapper();
+ }
+
+ /**
+ * Set the column separator character (default: comma).
+ *
+ * @param separator the separator character
+ * @return this builder for chaining
+ */
+ public CsvBuilder setSeparator(char separator) {
+ this.separator = separator;
+ return this;
+ }
+
+ /**
+ * Set the quote character (default: double-quote).
+ *
+ * @param quoteChar the quote character
+ * @return this builder for chaining
+ */
+ public CsvBuilder setQuoteChar(char quoteChar) {
+ this.quoteChar = quoteChar;
+ return this;
+ }
+
+ /**
+ * Convert a collection of maps to CSV.
+ * The keys of the first map are used as column headers.
+ *
+ * @param data the collection of maps
+ * @return the CSV string
+ */
+ public static String toCsv(Collection extends Map> data) {
+ if (data == null || data.isEmpty()) {
+ return "";
+ }
+ CsvMapper csvMapper = new CsvMapper();
+ Map first = data.iterator().next();
+ CsvSchema.Builder schemaBuilder = CsvSchema.builder();
+ for (String key : first.keySet()) {
+ schemaBuilder.addColumn(key);
+ }
+ CsvSchema schema = schemaBuilder.build().withHeader();
+ try {
+ return csvMapper.writer(schema).writeValueAsString(data);
+ } catch (IOException e) {
+ throw new CsvRuntimeException(e);
+ }
+ }
+
+ /**
+ * Convert a collection of typed objects to CSV using Jackson databinding.
+ * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations.
+ *
+ * @param data the collection of objects
+ * @param type the object type (used to derive the schema)
+ * @param the object type
+ * @return the CSV string
+ */
+ public static String toCsv(Collection data, Class type) {
+ if (data == null || data.isEmpty()) {
+ return "";
+ }
+ CsvMapper csvMapper = new CsvMapper();
+ CsvSchema schema = csvMapper.schemaFor(type).withHeader();
+ try {
+ return csvMapper.writer(schema).writeValueAsString(data);
+ } catch (IOException e) {
+ throw new CsvRuntimeException(e);
+ }
+ }
+
+ /**
+ * Build CSV from a collection of maps.
+ *
+ * @param data the collection of maps
+ * @return this builder
+ */
+ public CsvBuilder call(Collection extends Map> data) {
+ if (data == null || data.isEmpty()) {
+ this.content = "";
+ return this;
+ }
+ Map first = data.iterator().next();
+ CsvSchema.Builder schemaBuilder = CsvSchema.builder()
+ .setColumnSeparator(separator)
+ .setQuoteChar(quoteChar);
+ for (String key : first.keySet()) {
+ schemaBuilder.addColumn(key);
+ }
+ CsvSchema schema = schemaBuilder.build().withHeader();
+ try {
+ this.content = mapper.writer(schema).writeValueAsString(data);
+ } catch (IOException e) {
+ throw new CsvRuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return content != null ? content : "";
+ }
+
+ @Override
+ public Writer writeTo(Writer out) throws IOException {
+ return out.append(toString());
+ }
+}
diff --git a/subprojects/groovy-csv/src/main/java/groovy/csv/CsvRuntimeException.java b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvRuntimeException.java
new file mode 100644
index 00000000000..c105074c71f
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvRuntimeException.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.csv;
+
+import groovy.lang.GroovyRuntimeException;
+import org.apache.groovy.lang.annotation.Incubating;
+
+/**
+ * Represents runtime exception occurred when parsing or building CSV
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public class CsvRuntimeException extends GroovyRuntimeException {
+ private static final long serialVersionUID = 2809672072790437945L;
+
+ public CsvRuntimeException(String msg) {
+ super(msg);
+ }
+
+ public CsvRuntimeException(Throwable cause) {
+ super(cause);
+ }
+
+ public CsvRuntimeException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/subprojects/groovy-csv/src/main/java/groovy/csv/CsvSlurper.java b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvSlurper.java
new file mode 100644
index 00000000000..d85536289ad
--- /dev/null
+++ b/subprojects/groovy-csv/src/main/java/groovy/csv/CsvSlurper.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.csv;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import org.apache.groovy.lang.annotation.Incubating;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a CSV parser.
+ *
+ * Usage:
+ *
+ * def csv = new groovy.csv.CsvSlurper().parseText('name,age\nAlice,30\nBob,25')
+ * assert csv[0].name == 'Alice'
+ * assert csv[1].age == '25'
+ *
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public class CsvSlurper {
+ private final CsvMapper mapper;
+ private char separator = ',';
+ private char quoteChar = '"';
+ private boolean useHeader = true;
+
+ public CsvSlurper() {
+ this.mapper = new CsvMapper();
+ }
+
+ /**
+ * Set the column separator character (default: comma).
+ *
+ * @param separator the separator character
+ * @return this slurper for chaining
+ */
+ public CsvSlurper setSeparator(char separator) {
+ this.separator = separator;
+ return this;
+ }
+
+ /**
+ * Set the quote character (default: double-quote).
+ *
+ * @param quoteChar the quote character
+ * @return this slurper for chaining
+ */
+ public CsvSlurper setQuoteChar(char quoteChar) {
+ this.quoteChar = quoteChar;
+ return this;
+ }
+
+ /**
+ * Set whether the first row is a header row (default: true).
+ *
+ * @param useHeader true to treat the first row as headers
+ * @return this slurper for chaining
+ */
+ public CsvSlurper setUseHeader(boolean useHeader) {
+ this.useHeader = useHeader;
+ return this;
+ }
+
+ /**
+ * Parse the content of the specified CSV text.
+ *
+ * @param csv the CSV text
+ * @return a list of maps (one per row), keyed by column headers
+ */
+ public List