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



By grokonez | June 25, 2017.

Last updated on April 25, 2021.



Related Posts


Got Something To Say:

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

*