Spring JsonView unnest nested fields - java

I have something similar to these two classes:
public class User {
#JsonView(UserView.IdOnly.class)
int userID;
String name;
}
public class Project {
#JsonView(ProjectView.IdOnly.class)
int projectID;
#JsonView(ProjectView.Summary.class)
// JPA annotations ommitted
User user;
}
And the following View classes:
public class UserView extends View {}
public class ProjectView extends View {
public interface Summary extends IdOnly {}
}
public class View {
public interface IdOnly {}
}
My controller is as follows:
#JsonView(ProjectView.Summary.class)
#RequestMapping(value="/project/", method = RequestMethod.GET)
public List getProjects() {
return repository.findAll();
}
The JSON output, as you can see, wraps the userID inside the User object:
[
{
"projectID": 1,
"user": {
"userID": 1
}
}
]
This works as expected, and I can have my clients work with it, but it doesn't seem finished... I would like to get rid of the "user" wrapper:
[
{
"projectID": 1,
"userID": 1
}
]
Is there a way to do this cleanly? Preferably by using another annotation. I don't have any custom serializers yet and I would hate to have to start using them. If this can't be done with JsonViews, is there an alternative?
I know one solution would be to add a userID field to the Project class, but the setter would need a call to the repository (to also update the user field) which would mess up my class diagram.

Looks like there's an annotation called #JsonUnwrapped that removes the object wrapper, and all properties within it are included in the parent object.
http://fasterxml.github.io/jackson-annotations/javadoc/2.0.0/com/fasterxml/jackson/annotation/JsonUnwrapped.html
Hope this helps.

Related

How can I force Spring to serialize my controller's #ResponseBody as the method's declared return type?

I am attempting to use interfaces to define flexible response bodies from my Spring controllers.
What I expect: When I call an endpoint using Curl/Postman/etc, I should receive JSON objects that contain only the fields visible in the interface that the controller returns.
What I'm getting: When I call either endpoint, I receive JSON objects with every field defined in my entity.
Let's say my entity looks like this:
MyEntity.java
public class MyEntity implements ListEntityResponse, GetEntityResponse {
int dbid;
String name;
String description;
public int getDbid() { return dbid; }
public String getName() { return name; }
public String getDescription() { return description; }
}
Let's say MyEntity has many more fields that include complex data types that aren't suitable for serialization as part of a large list, or for certain other use cases. To solve this problem, I've created interfaces to limit which fields are visible in the response object. In this example, the first interface only defines two of the three getters, while the second interface defines all of them.
ListEntityResponse interface:
public interface ListEntityResponse {
int getDbid();
String getName();
}
GetEntityResponse interface:
public interface GetEntityResponse {
int getDbid();
String getName();
String getDescription();
}
And finally, here are my controllers. The important part is that each defines its return type as one of the interfaces:
ListEntityController
#GetMapping(path="/{name}")
public #ResponseBody List<ListEntityResponse> getList() {
return handler.getList(name);
}
GetEntityController
#GetMapping(path="/{name}")
public #ResponseBody GetEntityResponse getByName(#PathVariable("name") String name) {
return handler.getByName(name);
}
To recap, if we assume that our handler returns MyEntity objects, then I want that object to be serialized by Spring as the interface defined in the controller's return type. E.G. each JSON object in the list returned by the ListEntityController should have only the dbid and name fields. Unfortunately, that's not happening, and the returned JSON objects have every field available despite being masked as interface objects.
I have attempted to add #JsonSerialize(as = ListEntityResponse.class) to my first interface, and a similar annotation to the second. This works only if the entity implements just one of those interfaces. Once the entity implements multiple interfaces, each annotated with #JsonSerialize, Spring will serialize it as the first interface in the list regardless of the controller's return type.
How can I force a Spring to always serialize its Controller's responses as the controller function's return type?
Note: I am trying to find a solution that does not require me to use #JsonIgnore or #JsonIgnoreProperties. Additionally, I am trying to find a solution that does not require me to add #JsonView to my entity classes. I am willing to use the #JsonView annotation in the interfaces, but don't see a clean and maintainable way to do so.
How can I force Spring to always serialize its controller's responses as
the controller function's return type?
Please note that I am not interested in using #JsonIgnore,
#JsonIgnoreProperties, or #JsonView to provide the view masking that I
require. They do not fit my use case.
One of the options would be to create a thin wrapper over MyEntity class, which would be responsible for providing the required serialization-shape.
Every shape would be represented by its own wrapper, implemented as a single-field class. To specify serialization-shape, we can use as property of the #JsonSerialize annotation, by assigning the target interface as a value. And since we don't need the wrapper itself to reflected in the resulting JSON, we can make use of the #JsonUnwrapped annotation.
Here's a wrapper for GetEntityResponse shape:
#AllArgsConstructor
public class GetEntityResponseWrapper implements EntityWrapper {
#JsonSerialize(as = GetEntityResponse.class)
#JsonUnwrapped
private MyEntity entity;
}
And that's a wrapper for ListEntityResponse shape:
#AllArgsConstructor
public class ListEntityResponseWrapper implements EntityWrapper {
#JsonSerialize(as = ListEntityResponse.class)
#JsonUnwrapped
private MyEntity entity;
}
Basically, we have finished with serialization logic.
And you can use these lean classes in your controllers as is. But to make the solution more organized and easier to extend, I've introduced a level of abstraction. As you probably noticed both wrapper-classes are implementing EntityWrapper interface, its goal is to abstract away the concrete implementation representing shapes from the code in Controllers/Services.
public interface EntityWrapper {
enum Type { LIST_ENTITY, GET_ENTITY } // each type represents a concrete implementation
static EntityWrapper wrap(Type type, MyEntity entity) {
return switch (type) {
case LIST_ENTITY -> new ListEntityResponseWrapper(entity);
case GET_ENTITY -> new GetEntityResponseWrapper(entity);
};
}
static List<EntityWrapper> wrapAll(Type type, MyEntity... entities) {
return Arrays.stream(entities)
.map(entity -> wrap(type, entity))
.toList();
}
}
Methods EntityWrapper.wrap() and EntityWrapper.wrapAll() are uniform entry points. We can use an enum to represent the target type.
Note that EntityWrapper needs to be used in the return types in your Controller.
Here the two dummy end-points I've used for testing (I've removed the path-variables since they are not related to what I'm going to demonstrate):
#GetMapping("/a")
public List<EntityWrapper> getList() {
// your logic here
return EntityWrapper.wrapAll(
EntityWrapper.Type.LIST_ENTITY,
new MyEntity(1, "Alice", "A"),
new MyEntity(2, "Bob", "B"),
new MyEntity(3, "Carol", "C")
);
}
#GetMapping("/b")
public EntityWrapper getByName() {
// your logic here
return EntityWrapper.wrap(
EntityWrapper.Type.GET_ENTITY,
new MyEntity(2, "Bob", "B")
);
}
Response of the end-point "/a" (only two properties have been serialized):
[
{
"name": "Alice",
"dbid": 1
},
{
"name": "Bob",
"dbid": 2
},
{
"name": "Carol",
"dbid": 3
}
]
Response of the end-point "/b" (all three properties have been serialized):
{
"name": "Bob",
"description": "B",
"dbid": 2
}

Object mapper removing empty and null values when converting object to string Java

In response to api call, i'm sending Json Class Object as response.
I need response like this without empty objects being removed.
{
"links": {
"products": [],
"packages": []
},
"embedded":{
"products": [],
"packages": []
}
}
but final Response is looking like this
{
"links": {},
"embedded": {}
}
Two things to be aware of:
null and empty are different things.
AFAIK Jackson is configured to serialize properties with null values by default.
Make sure to properly initialize your properties in your object. For example:
class Dto {
private Link link;
private Embedded embedded;
//constructor, getters and setters...
}
class Link {
//by default these will be empty instead of null
private List<Product> products = new ArrayList<>();
private List<Package> packages = new ArrayList<>();
//constructor, getters and setters...
}
Make sure your classes are not extending another class with this annotation #JsonInclude(JsonInclude.Include.NON_NULL). Example:
//It tells Jackson to exclude any property with null values from being serialized
#JsonInclude(JsonInclude.Include.NON_NULL)
class BaseClass {
}
//Any property with null value will follow the rules stated in BaseClass
class Dto extends BaseClass {
private Link link;
private Embedded embedded;
//constructor, getters and setters...
}
class Link extends BaseClass {
/* rest of the design */
}
If you have the latter and you cannot edit BaseClass then you can define different rules in the specific classes:
class Link extends BaseClass{
//no matter what rules are defined elsewhere, this field will be serialized
#JsonInclude(JsonInclude.Include.ALWAYS)
private List<Product> products;
//same here
#JsonInclude(JsonInclude.Include.ALWAYS)
private List<Package> packages;
//constructor, getters and setters...
}

Spring Data JPA mapping nested entities

I'm a little bit confused about using projections in Spring Data JPA.
I wanted to optimize my queries by requesting only needed columns (preferably) in one query, and I thought that using projections is a good idea. But it seems that projection with nested projection becomes open and requests all columns and further nesting is impossible.
I've tried to find a solution with #Query (cannot find how to map nested lists), #EntityGraph (cannot find how to request only specified column) and #SqlResultSetMapping (cannot find how to make mapping nested lists), but it hasn't worked for me.
Is there any solution except receiving List<Object[]> and manually mapping?
I have the next entities classes (simplified for the question):
public class TestAttempt{
private Long id;
private User targetUser;
private Test test;
}
public class Test{
private Long id;
private String name;
private Set<Question> questions;
}
public class Question{
private Long id;
private String name;
private Test test;
}
And I wanted to write something like this (it can be just TestAttempt with null in unused fields):
public interface TestAttemptList {
Long getId();
Test getTest();
interface Test {
String getName();
List<Question> getQuestions();
interface Question {
String getName();
}
}
}
public interface TestAttemptRepository extends JpaRepository<TestAttempt, Long> {
List<TestAttemptList> getAllByTargetUserId(Long targetUserId);
}
And in result get something like this:
{
id: 1,
test: {
name: test1,
questions: [{
name: quest1
}, {
name: quest2
}]
}
}
Ive done something like this... You'll have your repository interfaces which will extend CrudRepository et. al. with the full objects (TestAttempt etc) You define your projections separately. The projection interfaces can contain other projection interfaces (TestAttemptSummary can contain a TestSummary) When the projection interface is used within the given repository the defined methods are applied to the object type the repository is configured for. Something like this.
public interface TestAttemptSummary {
Long getId();
TestSummary getTest();
}
public interface TestSummary {
String getName();
List<QuestionSummary> getQuestions();
}
public interface QuestionSummary {
String getName();
}
public interface TestAttemptRepository extends CrudRepository<TestAttempt, Long> {
TestAttemptSummary getTestAttemptSummary();
}

Partial update with Spring Data Elasticsearch repository

I have a document with many fields (some nested) indexed on elasticsearch. For example:
{
"id" : 1,
"username" : "...",
"name" : "...",
"surname" : "...",
"address" : "...",
"age": 42,
...
"bookmarks" : [{...}, {...}],
"tags" : [{...}, {...}]
}
Only some filed is mapped in my entity (I don't want to map the entire document):
#Document(indexName = "...", type = "...")
public class User {
#Id
private int id;
private String username;
private String address;
// getter/setter methods
}
In the service class I would like to do a partial update with ElasticsearchRepository, without mapping all document's fields in the entity:
public class UserServiceClass {
#Autowired
private UserElasticsearchRepository userElasticsearchRepository;
public void updateAddress(int id, String updatedAddress) {
User user = userElasticsearchRepository.findOne(id);
user.setAddress(updatedAddress);
userElasticsearchRepository.save(user);
}
}
but save method overwrites the entire document:
{
"id" : 1,
"username" : "...",
"address" : "..."
}
Partial udpdate seems not supported by ElasticsearchRepository. So I used ElasticsearchTemplate, to make a partial update, for example:
public class UserServiceClass {
#Autowired
private UserElasticsearchRepository userElasticsearchRepository;
#Autowired
private ElasticsearchTemplate elasticsearchTemplate;
public void updateAddress(int id, String updatedAddress) {
User user = userElasticsearchRepository.findOne(id);
if (user.getUsername().equals("system")) {
return;
}
IndexRequest indexRequest = new IndexRequest();
indexRequest.source("address", updatedAddress);
UpdateQuery updateQuery = new UpdateQueryBuilder().withId(user.getId()).withClass(User.class).withIndexRequest(indexRequest).build();
elasticsearchTemplate.update(updateQuery);
}
}
but seems a bit redundant to have two similar references (repository and ElasticsearchTemplate).
Can anyone suggest me a better solution?
Instead of having both ElasticsearchTemplate and UserElasticsearchRepository injected into your UserServiceClass, you can implement your own custom repository and let your existing UserElasticsearchRepository extend it.
I assume that your existing UserElasticsearchRepository look something like this.
public interface UserElasticsearchRepository extends ElasticsearchRepository<User, String> {
....
}
You have to create new interface name UserElasticsearchRepositoryCustom. Inside this interface you can list your custom query method.
public interface UserElasticsearchRepositoryCustom {
public void updateAddress(User user, String updatedAddress);
}
Then implement your UserElasticsearchRepositoryCustom by create a class called UserElasticsearchRepositoryImpl and implement your custom method inside with injected ElasticsearchTemplate
public class UserElasticsearchRepositoryImpl implements UserElasticsearchRepositoryCustom {
#Autowired
private ElasticsearchTemplate elasticsearchTemplate;
#Override
public void updateAddress(User user, String updatedAddress){
IndexRequest indexRequest = new IndexRequest();
indexRequest.source("address", updatedAddress);
UpdateQuery updateQuery = new UpdateQueryBuilder().withId(user.getId()).withClass(User.class).withIndexRequest(indexRequest).build();
elasticsearchTemplate.update(updateQuery);
}
}
After that, just extends your UserElasticsearchRepository with UserElasticsearchRepositoryCustom so it should look like this.
public interface UserElasticsearchRepository extends ElasticsearchRepository<User, String>, UserElasticsearchRepositoryCustom {
....
}
Finally, you service code should look like this.
public class UserServiceClass {
#Autowired
private UserElasticsearchRepository userElasticsearchRepository;
public void updateAddress(int id, String updatedAddress) {
User user = userElasticsearchRepository.findOne(id);
if (user.getUsername().equals("system")) {
return;
}
userElasticsearchRepository.updateAddress(user,updatedAddress);
}
}
You can also move your user finding logic into the custom repository logic as well so that you can passing only user id and address in the method. Hope this is helpful.
You can use ElasticSearchTemplate also to get your User object instead of repository interface. you can use NativeSearchQueryBuilder and other classes to build your query. With this you can avoid two similar reference from your class. Let me know if this solves your problem.

Spring Data Rest projection for Embedded Entities

Lets assume I have the following entities:
#Entity
public class Registration {
#ManyToOne
private Student student;
//getters, setters
}
#Entity
public class Student {
private String id;
private String userName;
private String name;
private String surname;
//getters, setters
}
#Projection(name="minimal", types = {Registration.class, Student.class})
public interface RegistrationProjection {
String getUserName();
Student getStudent();
}
I'm trying to create the following JSON representation, So when I use http://localhost:8080/api/registrations?projection=minimal I dont need all the user data to come along:
{
"_links": {
"self": {
"href": "http://localhost:8080/api/registrations{?page,size,sort,projection}",
}
},
"_embedded": {
"registrations": [
{
"student": {
"userName": "user1"
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/api/registrations/1{?projection}",
}
}
}
}
However the Projection I have created I get an exception(it works without the getUserName() statement. Obviously I have defined the interface in the wrong way...but which is the correct way to do something like this?
EDIT: Exception is the following
Invalid property 'userName' of bean class [com.test.Registration]:
Could not find field for property during fallback access! (through reference chain: org.springframework.hateoas.PagedResources["_embedded"]
->java.util.UnmodifiableMap["registrations"]->java.util.ArrayList[0]->org.springframework.data.rest.webmvc.json.["content"]
->$Proxy119["userName"])</div></body></html>
As exception said userName is not a member of Registration entity. That why it failed. But I think you already understand that.
First at all if you just want to expose its projection for Registration you should first change the following line:
#Projection(name="minimal", types = {Registration.class, Student.class})
By setting types = {Registration.class, Student.class} you asked Spring Data Rest to apply projection on Registration.class and Student.class. And that can cause some issue because depending of the type you should not have the same member/methods. In practice types must share a common ancestor.
Otherwise for the main problem you should try virtual projections the following thing (I didn't try on your sample but it should work, I hope):
#Projection(name="minimal", types = {Registration.class})
public interface RegistrationProjection {
#Value("#{target.getStudent().getUserName()}")
String getUserName();
}
I don't know if you are familiar with SPeL but the previous code just create a getUserName like this.getStudent().getUserName() because target is bound on the instance object (see documentation).

Categories

Resources