Integrate Spring RestAPIs with @JsonView

In the previous post, We had learned how to use @JsonView to serialize/de-serialize Java objects. Now we go to next step, JavaSampleApproach shows you how to integrate Spring RestAPIs with @JsonView to customize views and resolve infinity loop problem.

Related posts:
How to use @JsonView to serialize/de-serialize and customize JSON format from Java Object.
How to resolve Json Infinite Recursion problems when working with Jackson

I. Technologies

– Java: 1.8
– Maven: 3.3.9
– Spring Tool Suite: Version 3.8.4.RELEASE
– Spring Boot: 1.5.4.RELEASE
– Jackson library
– Json library

II. Integrate Spring RestAPIs with @JsonView

For the tutorial, We create 2 models Company & Product which have one-to-many relationship.

Company


public class Company {
	private int id;
	
    private String name;
 
    private List products;
	
	...

Product


public class Product {
	private int id;
	
    private String name;
    
    private Company company;
 
	...

What problems when retrieving Company/Product objects with 2 RestAPIs {‘/get/company’, ‘/get/product’}:


@GetMapping("/get/company")
public Company getCompany(){
    ...
	return apple; 
}

@GetMapping("/get/product")
public Product getProduct(){
    ...
	return iphone7; 
}

-> We will face with Infinity Loop problem:

spring restapis jsonview - Infinity Loop problem

Exceptions thrown from server side:


...
	
java.lang.StackOverflowError: null
	at com.fasterxml.jackson.core.JsonProcessingException.(JsonProcessingException.java:41)
	at com.fasterxml.jackson.databind.JsonMappingException.(JsonMappingException.java:251)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:706)
	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:704)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:690)
	
...

Another question: How to customize returned views?

=> Solution: @JsonView can help us to resolve 2 above problems: infinity loop issue and customized views:


...

@JsonView(View.OveralView.class)
@GetMapping("/company/overalview")
public Company getOveralViewCompany(){
	return apple; 
}

@JsonView(View.ProductView.class)
@GetMapping("/product/view")
public Product getProductView(){
	return iphone7; 
}

...

III. Practice

We create a SpringBoot project with some RestAPIs which use @JsonView to customize returned views.

spring jsonview - project structure

Step to do:
– Create SpringBoot project
– Create JsonView class
– Create model classes
– Implement RestAPIs
– Configure logback for file
– Run and check results

1. Create SpringBoot project

Using Spring Tool Suite to create a Spring Starter Project, then add dependencies {web, jackson-databind, org.json}:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
	<groupId>org.json</groupId>
	<artifactId>json</artifactId>
</dependency>

2. Create JsonView class

– Create a JsonView class with 3 customized views {OveralView, DetailView, ProductView}


package com.javasampleapproach.jackson.jsonview;

public class View {
	public static interface OveralView {}
	public static interface DetailView extends OveralView {}
	public static interface ProductView{}
}

3. Create model classes

Create 2 models {Company, Product}

Company:


package com.javasampleapproach.jackson.model;

import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.fasterxml.jackson.annotation.JsonView;
import com.javasampleapproach.jackson.jsonview.View;

public class Company {
	@JsonView(View.DetailView.class)
	private int id;
	
	@JsonView({View.OveralView.class, View.ProductView.class})
    private String name;

	@JsonView(View.OveralView.class)
    private List products;
	
    public Company(){
    }
    
    public Company(int id, String name, List products){
    	this.id = id;
    	this.name = name;
    	this.products = products;
    }
    
    public void setId(int id){
    	this.id = id;
    }
    
    public int getId(){
    	return this.id;
    }
    
    // name
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    // products
    public void setProducts(List products){
    	this.products = products;
    }
    
    public List getProducts(){
    	return this.products;
    }

    /**
     * 
     * Show Overal View
     */
	public String overalViewString() throws JSONException {
        JSONObject jsonInfo = new JSONObject();
        jsonInfo.put("name", this.name);
        
        JSONArray productArray = new JSONArray();
        if(this.products != null){
            this.products.forEach(product->{
                JSONObject subJson = new JSONObject();
                try {
					subJson.put("name", product.getName());
				} catch (JSONException e) {}
                productArray.put(subJson);
            });
        }
        jsonInfo.put("products", productArray);
        
        return jsonInfo.toString();
	}
	
	/**
	 * 
	 * Show Detail View
	 */
	public String detailViewString() throws JSONException {
        JSONObject jsonInfo = new JSONObject();
        jsonInfo.put("id",this.id);
        jsonInfo.put("name",this.name);
        
        JSONArray productArray = new JSONArray();
        if(this.products != null){
            this.products.forEach(product->{
                JSONObject subJson = new JSONObject();
                try {
                	subJson.put("id", product.getId());
					subJson.put("name", product.getName());
				} catch (JSONException e) {}
                productArray.put(subJson);
            });
        }
        jsonInfo.put("products", productArray);
        
        return jsonInfo.toString();
	}

}

Product:


package com.javasampleapproach.jackson.model;

import org.json.JSONException;
import org.json.JSONObject;

import com.fasterxml.jackson.annotation.JsonView;
import com.javasampleapproach.jackson.jsonview.View;

public class Product {
	@JsonView(View.DetailView.class)
	private int id;
	
	@JsonView({View.OveralView.class, View.ProductView.class})
    private String name;
    
	@JsonView(View.ProductView.class)
    private Company company;
	
    public Product(){
    }
    
    public Product(int id, String name){
    	this.id = id;
    	this.name = name;
    }
    
    public Product(String name, Company company){
    	this.name = name;
    	this.company = company;
    }
    
    public void setId(int id){
    	this.id = id;
    }
    
    public int getId(){
    	return this.id;
    }
    
    // name
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    // products
    public void setCompany(Company company){
    	this.company = company;
    }
    
    public Company getCompany(){
    	return this.company;
    }
    
	public String toString() {
		String info = "";
		try {
			JSONObject jsonInfo = new JSONObject();
			jsonInfo.put("name", this.name);

			JSONObject companyObj = new JSONObject();
			companyObj.put("name", this.company.getName());
			jsonInfo.put("company", companyObj);

			info = jsonInfo.toString();
		} catch (JSONException e) {}
		
		return info;
	}
}

4. Implement RestAPIs

We implement a WebController which has 5 RestAPIs {‘/get/company’, ‘/get/product’, ‘/get/company/overalview’, ‘/get/company/detailview’, ‘/get/product/view’} and use @JsonView to customize returned views.


package com.javasampleapproach.jackson.web;

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

import javax.annotation.PostConstruct;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.annotation.JsonView;
import com.javasampleapproach.jackson.jsonview.View;
import com.javasampleapproach.jackson.model.Company;
import com.javasampleapproach.jackson.model.Product;

@RestController
@RequestMapping("/get")
public class WebController {

	private Company apple;
	private Product iphone7;
	private Product iPadPro;
	
	@PostConstruct
	public void initial(){
		iphone7 = new Product(1, "Iphone 7");
        iPadPro = new Product(2, "IPadPro");
        
        List appleProducts = new ArrayList(Arrays.asList(iphone7, iPadPro));
        
        apple = new Company(1, "Apple", appleProducts);
        
        iphone7.setCompany(apple);
        iPadPro.setCompany(apple);
	}
	
	/*
	 * URLs MAKE ERRORS {'/get/company', '/get/product'}
	 */
	@GetMapping("/company")
	public Company getCompany(){
		return apple; 
	}
	
	@GetMapping("/product")
	public Product getProduct(){
		return iphone7; 
	}
	
	/*
	 * Get Customized Views {'/get/company/overalview', '/get/company/detailview', '/get/product/view'}
	 */
	@JsonView(View.OveralView.class)
	@GetMapping("/company/overalview")
	public Company getOveralViewCompany(){
		return apple; 
	}
	
	@JsonView(View.DetailView.class)
	@GetMapping("/company/detailview")
	public Company getDetailViewCompany(){
		return apple; 
	}
	
	@JsonView(View.ProductView.class)
	@GetMapping("/product/view")
	public Product getProductView(){
		return iphone7; 
	}

}

5. Configure logback for file

Under folder /src/main/resources, we create a logback file logback-spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/file-appender.xml" />
    <root level="INFO">
        <appender-ref ref="FILE" />
    </root>
</configuration>

Open file application.properties, then add path for log file:


logging.file=jsa-app.log

6. Run and check results

Build and Run the SpringBoot project with commandlines: mvn clean install and mvn spring-boot:run

– Make a request: http://localhost:8080/get/company

-> We face with infinity loop problem:

spring restapis jsonview - Infinity Loop problem

Open the jsa-app.log, see exceptions from server-side:


...

java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
	at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:472)
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.sendServerError(DefaultHandlerExceptionResolver.java:520)
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMessageNotWritable(DefaultHandlerExceptionResolver.java:409)
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:147)
	at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:136)
	at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:74)
	at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1222)
	at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1034)

...
	
java.lang.StackOverflowError: null
	at com.fasterxml.jackson.core.JsonProcessingException.(JsonProcessingException.java:41)
	at com.fasterxml.jackson.databind.JsonMappingException.(JsonMappingException.java:251)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:706)
	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
	at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:704)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:690)
	
...
	
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: getOutputStream() has already been called for this response
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
...

java.lang.IllegalStateException: getOutputStream() has already been called for this response
	at org.apache.catalina.connector.Response.getWriter(Response.java:626)
	at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:211)
	at javax.servlet.ServletResponseWrapper.getWriter(ServletResponseWrapper.java:109)
	at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView.render(ErrorMvcAutoConfiguration.java:227)
	at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1286)
	at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1041)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:984)
	
...

The same issue infinity loop when we make a request: http://localhost:8080/get/product

– Make a request to get overal view of company: http://localhost:8080/get/company/overalview

-> Result:


{"name":"Apple","products":[{"name":"Iphone 7"},{"name":"IPadPro"}]}

– Make a request to get detail view of company: http://localhost:8080/get/company/detailview

-> Result:


{"id":1,"name":"Apple","products":[{"id":1,"name":"Iphone 7"},{"id":2,"name":"IPadPro"}]}

– Make a request to get product view: http://localhost:8080/get/product/view

-> Result:


{"name":"Iphone 7","company":{"name":"Apple"}}

IV. Source code

SpringJsonView

2 thoughts on “Integrate Spring RestAPIs with @JsonView”

  1. Definitely consider that that you said. Your favourite reason appeared to be on the net
    the simplest factor to take into accout of. I say to you,
    I certainly get annoyed while other people think
    about concerns that they plainly do not recognise about.
    You controlled to hit the nail upon the highest and defined out
    the entire thing without having side effect , other people can take
    a signal. Will likely be back to get more. Thank you

Leave a Reply

Your email address will not be published. Required fields are marked *