Welcome to a quick tutorial on different JSON to Java use cases in Spring Boot. If you have an existing Spring Boot application, you can add the classes listed below to it and follow along. If you don’t have an existing Spring Boot application, consider following the Spring Guide Building a RESTful web service first, in order to get to a place where you can experiment with the following.
Simple JSON object to mutable Java object
Perhaps the most common use case is allowing someone to send an HTTP POST request to an endpoint, and for the posted JSON to be converted automatically to a plain-old Java object (POJO). Here is an example that “just works” with Spring Boot and Jackson.
The JSON for this example is:
{
"brand": "Apple",
"model": "iPhone"
}
The corresponding Java object would be:
class Smartphone {
private String brand;
private String model;
public Smartphone(String brand, String model) {
this.brand = brand;
this.model = model;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}
And the corresponding controller:
@RestController
public class SmartphoneController {
private static Logger logger = LoggerFactory.getLogger(SmartphoneController.class);
@PostMapping("/smartphone")
public ResponseEntity<Smartphone> addSmartphone(@RequestBody Smartphone smartphone) {
logger.info("Received new smartphone: " + smartphone.getBrand() + " " + smartphone.getModel());
return ResponseEntity.ok(smartphone);
}
}
You can use a tool like Postman or curl to POST the JSON. Make sure you send the “Content-Type” header with the value “application/json” so that Spring Boot automatically parses it into an instance of Smartphone. Here’s what that would look like as a curl command:
curl --location --request POST 'localhost:8080/smartphone' \
--header 'Content-Type: application/json' \
--data-raw '{
"brand": "Apple",
"model": "iPhone"
}'
Once that request is sent, the application should predictably route the request to the controller, match the request to the Java object, parse it, and then, log the following:
Received new smartphone: Apple iPhone
Simple JSON object to immutable Java object
The same use case as above can be achieved with an immutable object. Immutable objects are better for use in multithreaded code and they also have the benefit of promoting more maintainable code in general.
Here is an immutable version of the above Smartphone class:
class Smartphone {
private final String brand;
private final String model;
@JsonCreator
public Smartphone(String brand, String model) {
this.brand = brand;
this.model = model;
}
public String getBrand() {
return brand;
}
public String getModel() {
return model;
}
}
Notice that the only differences between the mutable and immutable version of Smartphone.java are as follows:
The member variables are declared final.
There are no setters.
The constructor is annotated with the “@JsonCreator” annotation.
As you can see, it’s easy to create an immutable class to represent your JSON.
By changing the Smartphone.java class to this new version, but keeping the existing SmartphoneController.java class, you should be able to make the same POST request as before and observe the same result.
Because of their benefits, I will use immutable objects throughout the remainder of this article.
JSON object holding another JSON object
Sometimes, an object has another object within it. Treating the contained object as a static inner class of the POJO closely models this relationship between the objects.
Consider if the smartphone example had a “model” that was not a single String, but was instead another object with a model name and/or a model version:
{
"brand": "Apple",
"model": {
"name": "iPhone",
"version": "11 Pro"
}
}
It is common to represent “Model” as a static inner class. No other changes to how you treat that class are necessary. The inner class can be represented as either an immutable or mutable object. Here is the immutable version of Smartphone with an immutable static inner class to represent Model:
class Smartphone {
private final String brand;
private final Model model;
@JsonCreator
public Smartphone(String brand, Model model) {
this.brand = brand;
this.model = model;
}
public String getBrand() {
return brand;
}
public Model getModel() {
return model;
}
static class Model {
private final String name;
private final String version;
@JsonCreator
public Model(String name, String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
}
}
If you use this version of Smartphone.java with your service, make sure and update the information that SmartphoneController.java logs so that you can see all the data that is passed. Here’s an example curl that will POST the above object:
curl --location --request POST 'localhost:8080/smartphone' \
--header 'Content-Type: application/json' \
--data-raw '{
"brand": "Apple",
"model": {
"name": "iPhone",
"version": "11 Pro"
}
}'
Post a JSON object with an array
What if someone adds a “features” attribute to this JSON, which is an array of strings describing different features of each phone? Like this example:
{
"brand": "Apple",
"model": {
"name": "iPhone",
"version": "11 Pro"
},
"features": ["Super Retina XDR display", "12 MP camera", "Up to 512 GB capacity"]
}
Jackson can parse a JSON array into either an array or a collection, like a List. We can take our existing Smartphone.java from the prior example and add a new features member with type List<String>
.
class Smartphone {
private final String brand;
private final Model model;
private final List<String> features;
@JsonCreator
public Smartphone(String brand, Model model, List<String> features) {
this.brand = brand;
this.model = model;
this.features = features;
}
public String getBrand() {
return brand;
}
public Model getModel() {
return model;
}
public List<String> getFeatures() {
return features;
}
static class Model {
private final String name;
private final String version;
@JsonCreator
public Model(String name, String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
}
}
No change needs to be made to the controller at all, but to help you visualize the result, try adding a log statement to the addSmartPhone method in order to log the features received, like line 4 here:
@PostMapping("/smartphone")
public ResponseEntity<Smartphone> addSmartphone(@RequestBody Smartphone smartphone) {
logger.info("Received new smartphone: " + smartphone.getBrand() + " " + smartphone.getModel().getName() + " " + smartphone.getModel().getVersion());
logger.info("The features of the smartphone are " + smartphone.getFeatures().stream().collect(Collectors.joining(", ")));
return ResponseEntity.ok(smartphone);
}
With that code in place, recompile and run the application. When you post the JSON above to the /smartphone endpoint, the application will log:
Received new smartphone: Apple iPhone 11 Pro
The features of the smartphone are Super Retina XDR display, 12 MP camera, Up to 512 GB capacity
Post a JSON array to the endpoint
Sometimes the JSON posted is not an object, but an array. Let’s say that we would like a new endpoint on our service that can receive multiple smartphones at once. JSON would look like this:
[{
"brand": "Apple",
"model": {
"name": "iPhone",
"version": "11 Pro"
},
"features": ["Super Retina XDR display", "12 MP camera", "Up to 512 GB capacity"]
},
{
"brand": "Samsung",
"model": {
"name": "Galaxy",
"version": "S20"
},
"features": ["6.2\" screen", "64MP camera", "4000 mAh battery"]
}]
We need to add a new method to our SmartphoneController to take the list of phones. Since both will now share the capability of logging the object received, we refactor that out into a private method. We now have this:
@RestController
public class SmartphoneController {
private static Logger logger = LoggerFactory.getLogger(SmartphoneController.class);
@PostMapping("/smartphone")
public ResponseEntity<Smartphone> addSmartphone(@RequestBody Smartphone smartphone) {
logSmartPhone(smartphone);
return ResponseEntity.ok(smartphone);
}
@PostMapping("/smartphones")
public ResponseEntity<List<Smartphone>> addSmartphones(@RequestBody List<Smartphone> smartphones) {
smartphones.forEach(this::logSmartPhone);
return ResponseEntity.ok(smartphones);
}
private void logSmartPhone(Smartphone smartphone) {
logger.info("Received new smartphone: " + smartphone.getBrand() + " " + smartphone.getModel().getName() + " " + smartphone.getModel().getVersion());
logger.info("The features of the smartphone are " + smartphone.getFeatures().stream().collect(Collectors.joining()));
}
}
Our POJO in Smartphone.java doesn’t change at all.
The key things to notice about the new “addSmartphones” method is that the relevant types have changed:
The method parameter “smartphones” is of type List
The return type is
ResponseEntity<List<Smartphone>>
.When you post the JSON provided above to this endpoint (make sure you post to /smartphones not just /smartphone) you will then see the expected logging:
Received new smartphone: Apple iPhone 11 Pro
The features of the smartphone are Super Retina XDR display, 12 MP camera, Up to 512 GB capacity
Received new smartphone: Samsung Galaxy S20
The features of the smartphone are 6.2" screen, 64MP camera, 4000 mAh battery
Wrapped JSON object
Sometimes the JSON object is wrapped in a thin JSON object “wrapper.” Taking our original smartphone object, wrapping it would be to make that object itself an attribute of an outer object like this:
{
"smartphone": {
"brand": "Apple",
"model": {
"name": "iPhone",
"version": "11 Pro"
},
"features": [
"Super Retina XDR display",
"12 MP camera",
"Up to 512 GB capacity"
]
}
}
The obvious way to handle JSON like that is to create another class, SmartphoneWrapper.java, that represents the wrapper:
class SmartphoneWrapper {
private final Smartphone smartphone;
@JsonCreator
public SmartphoneWrapper(Smartphone smartphone) {
this.smartphone = smartphone;
}
public Smartphone getSmartphone() {
return smartphone;
}
}
You could even make Smartphone a static inner class of this wrapper.
There’s a small wrinkle with this approach which is that all resulting code is now complicated with “reaching through” the wrapper to the smartphone inside. For example, the SmartphoneController.addSmartphone method might end up looking like this now:
@PostMapping("/smartphone")
public ResponseEntity<SmartphoneWrapper> addSmartphone(@RequestBody SmartphoneWrapper smartphone) {
logger.info("Received new smartphone: " + smartphone.getSmartphone().getBrand() + " " + smartphone.getSmartphone().getModel().getName() + " " + smartphone.getSmartphone().getModel().getVersion());
logger.info("The features of the smartphone are " + smartphone.getSmartphone().getFeatures().stream().collect(Collectors.joining(", ")));
return ResponseEntity.ok(smartphone);
}
From the controller’s standpoint, we don’t care about that wrapper. We might want to throw it out when we receive JSON like that, and keep dealing with just the object inside.
Jackson offers a feature to handle this called root element wrapping. Unfortunately, to my knowledge, it can only be turned on or off at the application level. So first, you might add the following settings to the application.properties of your Spring Boot application:
spring.jackson.serialization.wrap-root-value=true
spring.jackson.deserialization.unwrap-root-value=true
With those settings applied to the application, we now need to tell Jackson that the “smartphone” attribute in our JSON holds the smartphone object. To do that, we go to Smartphone.java and add the annotation “@JsonRootName(“smartphone”).” Everything else is still the same in Smartphone.java:
@JsonRootName("smartphone")
class Smartphone {
private final String brand;
private final Model model;
private final List<String> features;
@JsonCreator
public Smartphone(String brand, Model model, List<String> features) {
this.brand = brand;
this.model = model;
this.features = features;
}
public String getBrand() {
return brand;
}
public Model getModel() {
return model;
}
public List<String> getFeatures() {
return features;
}
static class Model {
private final String name;
private final String version;
@JsonCreator
public Model(String name, String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
}
}
You don’t need to change the SmartphoneController. It should look like this still:
@PostMapping("/smartphone")
public ResponseEntity<Smartphone> addSmartphone(@RequestBody Smartphone smartphone) {
logger.info("Received new smartphone: " + smartphone.getBrand() + " " + smartphone.getModel().getName() + " " + smartphone.getModel().getVersion());
logger.info("The features of the smartphone are " + smartphone.getFeatures().stream().collect(Collectors.joining(", ")));
return ResponseEntity.ok(smartphone);
}
Try rebuilding the application now and test it out. Send the JSON from the start of this section as your request body. Everything works out great!
The drawback to root wrapping
Because of that change in settings, and the fact that it takes place across the entire application, any other request bodies sent to endpoints in other parts of the application will always have to be wrapped. If you attempt to use a POJO like we’ve been using throughout the rest of this article in another controller, you will see an exception similar to the following:
[org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Root name 'brand' does not match expected ('smartphone') for type [simple type, class com.scottshipp.code.restservice.Smartphone]; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Root name 'brand' does not match expected ('smartphone') for type [simple type, class com.scottshipp.code.restservice.Smartphone]
at [Source: (PushbackInputStream); line: 2, column: 5] (through reference chain: com.scottshipp.code.restservice.Smartphone["brand"])]
For that reason, I prefer not to use root wrapping and just put up with the “reaching through” problem of a custom wrapper object. Your mileage may vary.
Conclusion
I hope this walkthrough of common Spring Boot JSON parsing use cases was useful. Did I miss any? Was anything unclear? Any other comments or questions? Let me know in the comment section below.
Top comments (0)