1. Introduction
A Comma Separated Values (CSV) files are delimited text files that use a comma to separate values (mostly, but other characters might be used as well). In such files, each line is a data record.
You might ask yourself, why should you even care about such files? Well, the answer is quite simple: these kinds of files are often used for exchanging the data between different systems. You can be sure, that most of the real-life applications, will require you to process CSV files.
In this pretty short tutorial, we will learn how to upload CSV files in Spring Boot REST API with Kotlin programming language and OpenCSV library.
2. Prepare Test Data
Before we will jump to the Spring Boot project, let’s prepare the test data. I’ve already prepared them for you, so you can just download this CSV file. Below, you can see the structure of the data:
Please notice, that this file matches the model of the data we will be using in our application (but I highly recommend for you to experiment a little and prepare your own data).
3. Imports
With that being done, we can switch to our Spring Boot project and add the required dependencies:
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.opencsv:opencsv:5.2")
4. Prepare Models
4.1. Create a User Class
As the first step, let’s prepare the User class structure, which will represent the row of the CSV file:
data class User( var id: Long? = null, var firstName: String? = null, var lastName: String? = null, var email: String? = null, var phoneNumber: String? = null, var age: Int? = null )
4.2. Create Custom Exceptions
This step of the tutorial is not necessary. However, I wanted to share a pretty cool Spring feature, which allows us to mark the exception class with the status code we want to return. Let’s implement two classes for later:
@ResponseStatus(HttpStatus.BAD_REQUEST) class BadRequestException(msg: String) : RuntimeException(msg) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) class CsvImportException(msg: String) : RuntimeException(msg)
As you probably guessed, the BadRequestException will return the 400 Bad Request status code to the user, while the CsvImportException will return the 500 Internal Server Error.
5. Implement CSV Service
As the next step, let’s implement the CSV service. Let’s start by adding a validator function:
@Service class CsvService { private fun throwIfFileEmpty(file: MultipartFile) { if (file.isEmpty) throw BadRequestException("Empty file") } }
This simple function will throw the BadRequestException if the file has no content or no file has been chosen in the multipart form.
Nextly, let’s add the createCSVToBean function:
private fun createCSVToBean(fileReader: BufferedReader?): CsvToBean<User> = CsvToBeanBuilder<User>(fileReader) .withType(User::class.java) .withIgnoreLeadingWhiteSpace(true) .build()
We used here the CsvToBeanBuilder class to configure the CsvToBean which will be used to convert the CSV data to objects. If you would like to know, what else can be configured with it, I highly recommend checking it’s documentation here.
The last helper function we will create is the closeFileReader:
private fun closeFileReader(fileReader: BufferedReader?) { try { fileReader!!.close() } catch (ex: IOException) { throw CsvImportException("Error during csv import") } }
All it does is closing the stream and releasing any system resources associated with it.
Finally, we can combine together all of the above and create the uploadCsvFile function:
fun uploadCsvFile(file: MultipartFile): List { throwIfFileEmpty(file) var fileReader: BufferedReader? = null try { fileReader = BufferedReader(InputStreamReader(file.inputStream)) val csvToBean = createCSVToBean(fileReader) return csvToBean.parse() } catch (ex: Exception) { throw CsvImportException("Error during csv import") } finally { closeFileReader(fileReader) } }
6. Configure CSV and Bean Binding
Before we will be able to parse our CSV input file, we need to specify how do we want the data to be bound to our bean. We can do that in several ways, but in this tutorial, I will show you the two most frequently used.
6.1. Bind With @CsvBindByName
This annotation specifies a binding between a column name of the CSV input and a field in a bean.
data class User( @CsvBindByName(column = "Id") var id: Long? = null, @CsvBindByName(column = "First Name") var firstName: String? = null, @CsvBindByName(column = "Last Name") var lastName: String? = null, @CsvBindByName(column = "Email") var email: String? = null, @CsvBindByName(column = "Phone number") var phoneNumber: String? = null, @CsvBindByName(column = "Age") var age: Int? = null )
Please notice here, that the name of the column must be identical to the name of the field.
6.2. Bind With @CsvBindByPosition
Another way is to bind the data based on a column number of the CSV input:
data class UserWithCsvBindByPosition( @CsvBindByPosition(position = 0) var id: Long? = null, @CsvBindByPosition(position = 1) var firstName: String? = null, @CsvBindByPosition(position = 2) var lastName: String? = null, @CsvBindByPosition(position = 3) var email: String? = null, @CsvBindByPosition(position = 4) var phoneNumber: String? = null, @CsvBindByPosition(position = 5) var age: Int? = null )
But that’s not all we need to do here. The first line in our CSV file contains headers. In this scenario, we have to configure our CsvToBean to skip it by adding the withSkipLines invocation:
private fun createCSVToBean(fileReader: BufferedReader?): CsvToBean = CsvToBeanBuilder(fileReader) .withType(User::class.java) .withSkipLines(1) .withIgnoreLeadingWhiteSpace(true) .build()
7. Create REST Controller
As the last step, we need to configure the REST endpoint. Let’s create the CsvController and add the uploadCsvFile function to it:
@RestController @RequestMapping("/api/csv") class CsvController( private val csvService: CsvService ) { @PostMapping fun uploadCsvFile( @RequestParam("file") file: MultipartFile ): ResponseEntity<List> { val importedEntries = csvService.uploadCsvFile(file) return ResponseEntity.ok(importedEntries) } }
A MultipartFile parameter is a representation of an uploaded file received in a multipart request.
8. Validation
Finally, we can check if everything works fine. Let’s figure out, how we can do that with Postman or curl command.
8.1. Testing Using Postman
All we need to do in Postman is to specify the URL, select the form-data as the request body type, and add our file with the appropriate key:
8.2. POSTing CSV File with cURL
If we would like to test with the command line, let’s specify the following:
curl -X POST -F 'file=@users.csv' http://localhost:8080/api/csv
8. Summary
And that would be all for today. This time, we’ve learned how to upload CSV files in Spring Boot REST API with Kotlin programming language and OpenCSV library.
I hope you will find this article useful. If you would like to share some thoughts on that with me or ask additional questions, I encourage you to do so by our fan page, group, or contact form.
For the source code, please visit this GitHub project.