Spring API - POST & persist data

Persist data in a JSON file. Blog post by Ionuț Alixăndroae

Ionut Alixandroae

October 21, 2021

9 min read

Spring API - How to persist data

This blog post is a continuation of the previous one and the presented code uses the functionality implemented there and it is built on top of it.

Click here to go to the first blog post from this Spring API series or access the GitHub repository.

What is a POST request?

A POST request is an HTTP method used to send data to a server or web service in order to create or update a resource.

Usually the data that is being sent is called a payload and can have different types: JSON, XML, raw text, etc.

POST action in Spring API

As demonstrated in the previous blog post, the controller class is mapping the HTTP action (GET or POST), and the data passed through with the help of dedicated annotations, to the model class that holds the logic.

What we want to do is to take a name property sent via a POST request and create a new Location object. This is very similar to the logic inside the GET request, with the difference that there the property was coming as a query parameter, not inside a JSON payload via a POST request. After the new object is created, we want to save it as a JSON object inside a new file called locations.json. And the request will then respond with the whole content inside this file, technically demonstrating how to add and persist data in a file using a POST request.

locations.json
{
  "locations": []
}

A new method marked with the @RestController annotation is added to the LocationController.java class. The @RestController annotation defines the web service's endpoint address and @RequestBody makes sure that the payload sent together with the request is passed as an argument to the save() method.

We have defined the argument of type JsonNode because the payload will be of type JSON and also because it is easier to parse it later and extract the needed property.

We are interested in the name property from inside the payload, and we can access it with the following code

LocationController.java
body.get("name").asText();
LocationController.java
Location newLocation = new Location(counter.incrementAndGet(), body.get("name").asText());

At this point, we have a new Locations object created dynamically based on user input (payload).

Save data in a file

Inside the model folder I created a new Java class named LocationsFile.java. The purpose of this class is to establish the connection to the locations.json file and to take the newly created Location object and append it there.

/model/LocationsFile.java
package com.ionutalixandroae.demorestapi.model;

import java.io.FileReader;
import java.io.FileWriter;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import com.fasterxml.jackson.databind.ObjectMapper;

public class LocationsFile {

    private final String filePath;
    private final String path = "/path_to/Spring/demo-rest-api/";

    public LocationsFile(String filePath) {
        this.filePath = String.format("%s%s", this.path, filePath);
    }

    public JSONObject saveToFile(Location newLocation) {
        // Get locations from file
        JSONArray locations = this.getLocations();

        // Prepare the objects to convert from Java object to string to JSON
        ObjectMapper mapper = new ObjectMapper();
        JSONParser parser = new JSONParser();

        // This object will hold the new and updated file content
        JSONObject fileContent = new JSONObject();

        try (FileWriter writer = new FileWriter(this.filePath)){
            String newLocationAsString = mapper.writeValueAsString(newLocation);
            locations.add(parser.parse(newLocationAsString));
            fileContent.put("locations", locations);
            writer.write(fileContent.toJSONString());
            return fileContent;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private JSONArray getLocations() {
        try (FileReader reader = new FileReader(this.filePath)) {
            JSONParser jsonParser = new JSONParser();
            Object fileContent = jsonParser.parse(reader);
            JSONObject jsonContent = (JSONObject) fileContent;
            return (JSONArray) jsonContent.get("locations");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

The class has two private properties, one is already initialized and the other one gets initialized inside the constructor. A private method called getLocations() is created to read the content of the file first, take all previously saved objects and return them in an array. This is possible using a FileReader object that opens the JSON file, parses the content until it extracts the needed array of data and then converts it into a native Java JSONArray object.

The public method called saveToFile() is accepting the Location object and appends it to the previously returned array, making sure every resource gets saved inside. Then, using a FileWriter, the entire content is persisted inside locations.json and then returned as a JSON response.

LocationController.java
LocationsFile locationsFile = new LocationsFile("locations.json");
return locationsFile.saveToFile(newLocation);

Spring Framework is capable of taking the array returned by the saveToFile() method and translate it into a JSON response for the user.

For every new request, a new object is created and saved inside the locations array in the locations.json file.

locations.json
{
  "locations": [
    {
      "locationInfo": {
        "extent": {
          "ymin": 47.99941000000007,
          "xmin": 11.440540000000055,
          "ymax": 48.27341000000007,
          "xmax": 11.714540000000056
        },
        "feature": {
          "geometry": {
            "x": 11.577540000000056,
            "y": 48.13641000000007
          },
          "attributes": {
            "Score": 100,
            "Addr_Type": "Locality"
          }
        },
        "name": "Munich, Bavaria"
      },
      "name": "Munich",
      "coordinates": {
        "latidude": 48.13641000000007,
        "longitude": 11.577540000000056
      },
      "id": 1
    },
    {
      "locationInfo": {
        "extent": {
          "ymin": 52.26304000000003,
          "xmin": 13.123910000000066,
          "ymax": 52.76904000000003,
          "xmax": 13.629910000000066
        },
        "feature": {
          "geometry": {
            "x": 13.376910000000066,
            "y": 52.51604000000003
          },
          "attributes": {
            "Score": 100,
            "Addr_Type": "Locality"
          }
        },
        "name": "Berlin"
      },
      "name": "Berlin",
      "coordinates": {
        "latidude": 52.51604000000003,
        "longitude": 13.376910000000066
      },
      "id": 2
    }
  ]
}

Conclusions

Implementing a new web service to handle POST actions is extremely easy with a few lines of code. Similarly, with the GET requests, Spring performs the hard work of mapping all data and properties and, by using the model-controller pattern, one can effortlessly create complex logic for data transactions and data persistence.

I used a file here for all data reading and writing operations, but this can be simply changed with a database, and then the web service will be better prepared for multi-user concurrent requests 💪

I would like to continue this series by approaching and implementing all CRUD operations so that the final code will be able to serve as a ready-to-use boilerplate for future use.

Up to date codebase can be found in the repository here.