This topic offers an in-depth overview of transforming Java and Kotlin classes into request bodies in REST controllers using Spring Boot. You'll learn how Spring Boot serializes objects into a network-friendly format.
Serialization
Methods within a @RestController class can return a variety of data types, including objects, collections, or even raw data types, such as String, int, and so on. Spring's HTTP message converters usually automatically convert the returned data into JSON format.
Here's an example of returning an object from a @RestController method:
Java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/person")
public Person getPerson() {
return new Person("John", "Doe", 25);
}
public class Person {
private String firstName;
private String lastName;
private int age;
// Constructors, getters and setters are omitted for brevity
}
}
Kotlin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class DemoController {
@GetMapping("/person")
fun getPerson(): Person = Person("John", "Doe",25)
}
data class Person(
val firstName: String,
val lastName: String,
val age: Int
)
In this example, when a GET /person request is made, the getPerson() method is invoked, returning a new Person object. Spring Boot then automatically serializes this Person object to a JSON string, which is included in the HTTP response body.
To return a collection, you can do it as follows:
Java
@GetMapping("/people")
public List<Person> getPeople() {
return Arrays.asList(
new Person("John", "Doe", 25),
new Person("Jane", "Doe", 24)
);
}
Kotlin
@GetMapping("/people")
fun getPerson(): List<Person> = listOf(
Person("John", "Doe", 25),
Person("Jane", "Doe", 24)
)
This method returns a list of Person objects, which are automatically serialized to a JSON array.
In Spring Boot, the Jackson library, which is included by default when you create a new Spring Boot project, typically handles the serialization of objects to JSON. Here's how it works:
When the getPerson() method returns a Person object, Spring Boot needs to convert this object into a format that can be included in the HTTP response body. Since HTTP is a text-based protocol, the Person object must be converted into a string. JSON is a popular text-based format for representing structured data, making it a natural choice for this purpose.
The Jackson library provides functionality for converting Java and Kotlin objects to JSON and vice versa. It uses reflection to inspect the properties of the Person object. For each property, it creates a key-value pair in the resulting JSON string. The key is the property's name, and the value is the property's value.
For example, if the Person object has properties firstName, lastName, and age with values "John", "Doe", and 25, respectively, the resulting JSON string would look like this:
{
"firstName": "John",
"lastName": "Doe",
"age": 25
}
This JSON string is then included in the HTTP response body. When the client receives the response, it can parse the JSON string to recreate the Person object in its own programming language.
By default, Jackson serializes all properties with a public getter method and deserializes from all properties with a public setter method. If a property has neither, it is ignored.
Nested objects
The Jackson library handles nested objects in a recursive manner. It traverses through the object graph and serializes each object it encounters into a corresponding JSON structure.
Let's say we have another class, Address, which is used in the Person class:
Java
public class Address {
private String street;
private String city;
private String country;
// Constructors, getters and setters are omitted for brevity
}
public class Person {
private String firstName;
private String lastName;
private int age;
private Address address;
// Constructors, getters and setters are omitted for brevity
}
Kotlin
data class Address(
val street: String,
val city: String,
val country: String
)
data class Person(
val firstName: String,
val lastName: String,
val age: Int,
val address: Address
)
If you return a Person object from a @RestController method, the address property will be serialized to a nested JSON object. For example, if you have a Person object like this:
Java
Person person = new Person("John", "Doe", 25, new Address("Olesvej", "Qaqortoq", "Greenland"))
Kotlin
val person: Person = Person("John", "Doe", 25, Address("Olesvej", "Qaqortoq", "Greenland"))
The resulting JSON string will look like this:
{
"firstName": "John",
"lastName": "Doe",
"age": 25,
"address": {
"street": "Olesvej",
"city": "Qaqortoq",
"country": "Greenland"
}
}
As you can see, the Address object is represented as a nested JSON object inside the Person object.
The same applies to collections of objects. If a property is a collection (for example, a List or Set), each element in the collection will be serialized to a JSON object, and the entire collection will be represented as a JSON array.
Jackson handles this nested serialization automatically, so you don't need to do anything special to enable it.
Circular references
Circular references pose a problem during serialization because they can lead to infinite loops. For example, consider two classes, Parent and Child, where a Parent object contains a list of Child objects, and each Child object has a reference back to the Parent object. When trying to serialize a Parent object, the serialization process will keep bouncing back and forth between the Parent and Child objects, resulting in a StackOverflowError.
Fortunately, the Jackson library provides several ways to handle circular references.
One way is to use the @JsonManagedReference and @JsonBackReference annotations. You can annotate the "parent" side of the relationship with @JsonManagedReference, and the "child" side of the relationship with @JsonBackReference. This tells Jackson to manage the relationship from the parent side and avoid serializing the back reference from the child side to prevent infinite recursion.
Here's an example:
Java
public class Parent {
private String name;
@JsonManagedReference
private List<Child> children;
// Constructors, getters and setters are omitted for brevity
}
public class Child {
private String name;
@JsonBackReference
private Parent parent;
// Constructors, getters and setters are omitted for brevity
}
Kotlin
data class Parent(
val name: String,
@JsonManagedReference
var children: List<Child> = emptyList()
)
data class Child(
val name: String,
@JsonBackReference
val parent: Parent
)
If you serialize a Parent object containing a list of Child object, you will get a JSON string like this:
{
"name": "parent",
"children": [
{
"name": "child 1"
},
{
"name": "child 2"
}
]
}JSON customization
There may be situations where some properties must be excluded from serialization. Jackson provides several ways to accomplish this, the simplest of which is to add the @JsonIgnore annotation to that property. This tells Jackson to ignore this property during serialization. Here's an example:
Java
public class Person {
private String firstName;
private String lastName;
@JsonIgnore
private String password;
// methods omitted for brevity
}
Kotlin
data class Person(
val firstName: String,
val lastName: String,
@JsonIgnore
val password: String
)
In this example, the password property will not be included in the serialized JSON:
{
"firstName": "John",
"lastName": "Doe"
}
As you can see, Jackson translates property names like they are written in an object. Sometimes, however, you need to set a different JSON key name for a certain property, for example, to change the camel case name to the snake case one.
In Jackson, you can change the serialized property name by using the @JsonProperty annotation. The @JsonProperty annotation allows you to specify the name to be used for the property in the JSON output.
Here is an example:
Java
public class Person {
@JsonProperty("first_name")
private String firstName;
@JsonProperty("last_name")
private String lastName;
private int age;
// Constructors, getters and setters are omitted for brevity
}
Kotlin
data class Person(
@JsonProperty("first_name")
val firstName: String,
@JsonProperty("last_name")
val lastName: String,
val age: Int
)
In this example, when a Person object is serialized, the firstName property will appear as first_name in the JSON output, and the lastName property will appear as last_name. The age property will keep its original name because it doesn't have a @JsonProperty annotation.
Conclusion
In this topic, you've delved into the details of object serialization in Spring Boot and briefly introduced the Jackson library. You've explored how you can handle complex scenarios such as nested objects and circular references. Additionally, you learned to control serialization more granularly by ignoring certain properties or renaming them as needed. This knowledge will help you effectively manage data representation in your Spring Boot applications.