spring

Spring REST with JSON Example

RESTful web services using JSON data format address the problems with SOAP web services using XML. REST can output data in different formats like Comma Separated Values (CSV) or Really Simple Syndication (RSS), but the most popular format is JSON. Compared to XML, JSON is not only more human readable but also lightweight. It’s easier for the browser to take a JSON data structure and get its JavaScript structure and the processing time is lesser. Development with SOAP involves more code and at times becomes unwieldy. The biggest advantage for the REST + JSON mechanism is a smaller learning curve for newbies, which further gets accelerated with Spring Boot.
 
 
 

1. Introduction

In this article, we will show how to build a RESTful web service that uses JSON format for data in the request and response to a Spring Boot application. The key aspect of the RESTful design is to conceptualize your data as resources. The resources could be anything from a map showing a location to a software download. The HTTP actions (verbs) are used in a meaningfully semantic manner in conjunction with the Universal Resource Indicator (URI) to deliver application functionality. This is best illustrated with database records.

2. Application

The application we will develop is a web service that handles Tickets as in a bug-tracking or task-tracking system. A Ticket has a description and many comments. A Comment has a Text field. In our application, data is persisted to an in-memory H2 database. We use Spring Data JPA for the database operations.

Thus, the web service will offer a RESTful interface to the database operations on a ticket. The Create (C), Read (R), Update (U) and Delete (D) operations are mapped to POST (Po), GET (G), PUT (Pu) and DELETE (D) actions of HTTP. To use the acronyms as a helpful mnemonic, the database CRUD is mapped to HTTP PoGPuD.

3. Environment

I have used the following technologies for this application:

  • Java 1.8
  • Spring Boot 1.5.9
  • Maven 3.3.9
  • Ubuntu 16.04 LTS

4. Source Code

This is a maven-based project, so all the project-level settings and dependencies are given in pom.xml file.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.javacodegeeks.webservices.rest</groupId>
    <artifactId>ticket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>ticket</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.properties

spring.h2.console.enabled=true

This configuration is required to enable browser access to the H2 database, since we are not using Spring Boot’s developer tools. Additionally, we made sure to include com.h2database:h2 is on the classpath via a dependency in pom.xml.

TicketApplication.java

package org.javacodegeeks.webservices.rest.ticket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TicketApplication {

	public static void main(String[] args) {
		SpringApplication.run(TicketApplication.class, args);
	}
}

This is the main class of the application that runs on the default Tomcat container of Spring Boot at port 8080.

Ticket.java

package org.javacodegeeks.webservices.rest.ticket.domain;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter @Setter @NoArgsConstructor
public class Ticket {
	
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	@Column(name="TICKET_ID")
	private Long ticketId;
	
	private String description;
	
	@OneToMany(mappedBy="ticket", cascade=CascadeType.ALL)
	private List comments = new ArrayList();
}

This is the chief domain class of the application. The @Entity annotation specifies that this class is mapped to a database table and since we don’t have the @Table annotation, the table name will be the same as the class name. The three lombok annotations, @Getter, @Setter, and @NoArgsConstructor respectively create the getters and setters to the fields and a default no-argument constructor.

The field ticketId is annotated with @Id, @GeneratedValue(strategy=GenerationType.AUTO) and @Column(name="TICKET_ID") specifying that it is the key column with the name TICKET_ID and whose value should be automatically generated.

A Ticket has many comments which are stored in an ArrayList. The annotation @OneToMany(mappedBy="ticket", cascade=CascadeType.ALL) specifies the database side of the relationship indicating that Ticket is the owner of the bi-directional relationship and that changes to a Ticket are to propagated to all the child records.

Comment.java

package org.javacodegeeks.webservices.rest.ticket.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import com.fasterxml.jackson.annotation.JsonIgnore;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter @Setter @NoArgsConstructor
public class Comment {
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	@Column(name="COMMENT_ID")
	private Long commentId;
	
	private String text;
	
	@ManyToOne(fetch=FetchType.LAZY)
	@JoinColumn(name="TICKET_ID")
	@JsonIgnore
	private Ticket ticket;
}

As in Ticket, this class also uses @Entity, @Getter, @Setter, @NoArgsConstructor, @Id, @GeneratedValue and @Column annotations. The important annotation here is @ManyToOne annotation indicating the reverse side of the relationship with Ticket. The @JoinColumn annotation specifies that the foreign key is TEXT_ID. The @JsonIgnore is used to avoid the parent record’s attributes getting parsed into the output.

TicketRepository.java

package org.javacodegeeks.webservices.rest.ticket.service;

import org.javacodegeeks.webservices.rest.ticket.domain.Ticket;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TicketRepository extends JpaRepository<Ticket, Long> {

}

The @Repository annotation on this interface allows it to import standard DAO routines into the runtime environment and also makes it eligible for Spring DataAccessException translation.

CommentRepository.java

package org.javacodegeeks.webservices.rest.ticket.service;

import org.javacodegeeks.webservices.rest.ticket.domain.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {

}

This is the repository interface for the Comment class using the @Repository annotation.

TicketEndpoint.java

package org.javacodegeeks.webservices.rest.ticket.endpoint;

import java.util.List;

import org.javacodegeeks.webservices.rest.ticket.domain.Comment;
import org.javacodegeeks.webservices.rest.ticket.domain.Ticket;
import org.javacodegeeks.webservices.rest.ticket.service.TicketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TicketEndpoint {

	@Autowired
	private TicketService ticketService;

	// --------------------------------------------
	// CRUD OPERATIONS FOR PARENT RECORDS (TICKETS)

	@PostMapping("/tickets")
	public Ticket createTicket(@RequestBody Ticket ticket) {
		Ticket savedTicket = ticketService.createTicket(ticket);
		return savedTicket;
	}

	@GetMapping("/tickets")
	public List getAllTickets() {
		return ticketService.findAll();
	}

	@GetMapping("/tickets/{id}")
	public Ticket getTicket(@PathVariable long id) {
		return ticketService.findTicket(id);
	}

	@PutMapping("/tickets/{id}")
	public Ticket changeTicket(@PathVariable long id, @RequestBody Ticket ticket) {
		return ticketService.updateTicket(id, ticket);
	}

	@DeleteMapping("/tickets/{id}")
	public String deleteTicket(@PathVariable long id) {
		ticketService.deleteById(id);
		return String.format("Ticket id #%d successfully deleted", id);
	}

	// --------------------------------------------
	// CRUD OPERATIONS FOR CHILD RECORDS (COMMENTS)

	@PostMapping("/tickets/{id}/comments")
	public Ticket createComment(@PathVariable long id, @RequestBody Comment comment) {
		return ticketService.createComment(id, comment);
	}

	@GetMapping("/tickets/{id}/comments")
	public List getAllComments(@PathVariable long id) {
		return ticketService.findAllComments(id);
	}

	@GetMapping("/tickets/comments/{id}")
	public Comment getComment(@PathVariable long id) {
		return ticketService.findComment(id);
	}

	@PutMapping("/tickets/comments/{id}")
	public Comment changeComment(@PathVariable long id, @RequestBody Comment comment) {
		return ticketService.updateComment(id, comment);
	}

	@DeleteMapping("/tickets/comments/{id}")
	public String deleteComment(@PathVariable long id) {
		ticketService.deleteCommentById(id);
		return String.format("Comment id %d successfully deleted", id);
	}
}

This class is an end point for the REST clients as specified by the @RestController annotation. A TicketService bean is auto wired into this class with the @Autowired annotation. The key design to note here is that it offers the endpoint to both the ticket and comment server side operations. The intuition behind this design is that comments do not have an independent existence; they belong to a Ticket. Therefore in the service class, there are ten methods five each for the ticket and comments functionality. The create methods createTicket and createComment are annotated with @PostMapping annotation, the read methods getAllTickets, getTicket, getAllComments and getComment are annotated with @GetMapping annotation, the update methods changeTicket and changeComment are annotated with @PutMapping annotation and finally, the delete methods deleteTicket and deleteComment are annotated with @DeleteMapping annotation. To reiterate, database CRUD is mapped to HTTP PoGPuD.

The @PathVariable annotation indicates the argument is part of the URI and the @RequestBody annotation specifies which object the HTTP body is de-serialized to.

TicketService.java

package org.javacodegeeks.webservices.rest.ticket.service;

import java.util.List;

import org.javacodegeeks.webservices.rest.ticket.domain.Comment;
import org.javacodegeeks.webservices.rest.ticket.domain.Ticket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TicketService {

	@Autowired
	private TicketRepository ticketRepository;

	// --------------------------------------------
	// CRUD OPERATIONS FOR PARENT RECORDS (TICKETS)

	public Ticket createTicket(Ticket ticket) {
		return ticketRepository.save(ticket);
	}

	public List findAll() {
		return ticketRepository.findAll();
	}

	public Ticket findTicket(long id) {
		return ticketRepository.findOne(id);
	}

	public Ticket updateTicket(long id, Ticket ticket) {
		Ticket updatedTicket = findTicket(id);
		if (!ticket.getDescription().equals(updatedTicket.getDescription())) {
			updatedTicket.setDescription(ticket.getDescription());
			return ticketRepository.save(updatedTicket);
		} else
			return null;
	}

	public void deleteById(long id) {
		ticketRepository.delete(id);
	}

	@Autowired
	private CommentRepository commentRepository;

	// --------------------------------------------
	// CRUD OPERATIONS FOR CHILD RECORDS (COMMENTS)

	public Ticket createComment(long ticketId, Comment comment) {
		Ticket ticket = findTicket(ticketId);
		comment.setTicket(ticket);
		ticket.getComments().add(comment);

		return ticketRepository.save(ticket);
	}

	public List findAllComments(long ticketId) {
		return findTicket(ticketId).getComments();
	}

	public Comment findComment(long id) {
		return commentRepository.findOne(id);
	}

	public Comment updateComment(long commentId, Comment comment) {
		Comment savedComment = commentRepository.findOne(commentId);
		savedComment.setText(comment.getText());
		commentRepository.save(savedComment);
		return savedComment;
	}

	public void deleteCommentById(long id) {
		commentRepository.delete(id);
	}
}

This is the business service class specified with the @Service annotation. It has two repository beans TicketRepository and CommentRepository autowired into it. The create methods invoke the repository save method. The findAll method invokes the repository findAll method. Similarly, the findTicket and deleteById method invoke the repository methods findOne and delete. The updateTicket method takes in an id value, fetches the ticket record from the database, and if the description is not the same as the one passed in with the request body, it changes the description and saves the changed record back in the database.
For the comments, the createComment method first fetches the parent ticket from the database, adds the comment to the ArrayList of comments and then invokes the repository save method to persist the record. The findAllComments method fetches the parent ticket by calling the findTicket method and returns the comments list by invoking the getter. The findComment and deleteCommentById methods invoke the repository methods findOne and delete respectively. The updateComment method takes in an id value, fetches the comment record from the database, sets the text to that passed in with the request body and saves the changed record back to the database.

5. How To Run and Test

In a terminal window, change directory to the root folder of the application ticket and enter

mvn spring-boot:run

This will start the application.

In another terminal window, change directory to ticket/src/main/resources and run the file data.sh. This file uses curl command to make POST requests “/tickets” and “/tickets/{id}/comments” to create three posts and three comments each for them.

You can check that these 12 records in the database. In a browser window, go the URL http://localhost:8080/h2-console/. Make sure the JDBC URL is jdbc:h2:mem:testdb. Hit Connect button.
In the next screen run the SQL statements SELECT * from TICKET; and SELECT * FROM COMMENT; to see the database records. The screenshots are given below.

H2 console showing records in TICKET table.

H2 console showing records in COMMENT table.

For the next steps, we can use any REST client like the Advanced REST Client chrome extension or even SoapUI. I used the Postman application. Let’s test with three REST calls to
i) Delete the second ticket
ii) Modify the second comment of the first ticket
iii) Delete the third comment of the third ticket

For i) we send a DELETE request to localhost:8080/tickets/2. This will delete the child comments records too. You should see a confirmation message, “Ticket id #2 successfully deleted” as the response.
For ii) we send a PUT request to localhost:8080/tickets/comments/2 since the id of the comment record in the database is 2. In Postman, in the Body panel, select the radio option raw and from the drop down list at the right, select JSON (application/json). For the input, enter “text” : “First ticket, modified second comment” and click Send. You should see the changed comment in the response box. The screenshot for this step is given below:

Postman screen showing request and response for step (ii).

For iii) we send a DELETE request to localhost:8080/tickets/comments/9 since the id of the comment record in the database is 9.

After doing these three steps, the database should have two tickets and five comments, one of which is modified from its original value. You can check them in the browser via h2-console or in the terminal window by running

curl -X GET http://localhost:8080/tickets | jq .

This command’s output will be as shown in the following screenshot

Tickets service final response in terminal window.

6. Summary

In this article, we have seen how to use Spring framework to implement a RESTful web service that uses JSON for the request and response. We have seen how to map the HTTP actions to database operations. There are many other aspects of REST web services that are vital in real world implementations. These aspects are security, versioning, connectedness using HATEOAS (Hypermedia As The Engine Of Application State), meaningful messages in Exceptions, internationalization and so on. This article covers the basics and positions you to explore those advance areas.

7. Useful Links

8. Download the Source Code

Download
You can download the full source code of this example here: ticket.zip

Mahboob Hussain

Mahboob Hussain graduated in Engineering from NIT Nagpur, India and has an MBA from Webster University, USA. He has executed roles in various aspects of software development and technical governance. He started with FORTRAN and has programmed in a variety of languages in his career, the mainstay of which has been Java. He is an associate editor in our team and has his personal homepage at http://bit.ly/mahboob
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button