Reputation: 2528
I have entity type as item in recursive tree, so any item has references to its parent and children (of same type)
public class Category {
private Integer id;
private String displayName;
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
@JsonIdentityReference(alwaysAsId=true)
private Category parent;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
@JsonIdentityReference(alwaysAsId=true)
private Set<Category> children;
// constructors, getters and setters
}
As you can see, I marked both reference fields with @JsonIdentityReference
annotation forcing them to render as plain ids. Currently with this setup an entity is rendered as follows:
// from .../categories/0
{
"id" : 0,
"displayName" : "Root",
"parent" : null,
"children" : [ 1, 13, 17 ]
}
(which is perfectly fine).
However, very common use scenario for client is to fetch whole sub-tree starting from specific node, which in this implementation requires several requests to server. I want to create another enpoint that allows client to perform this operation with single request.
@RestController
@RequestMapping(value = "/categories")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@Autowired
private CategoryRepository categoryRepo;
@RequestMapping(value = "/tree", method = RequestMethod.GET)
public Category getTree(@RequestParam(name = "root", required = false) Integer id) {
Category root = id == null ? categoryService.getRoot() : categoryRepo.findOne(id);
return categoryService.getTree(root);
}
// other endpoints
// getOne(id)
// getAll()
}
Response from this endpoint renders full objects only if I manually remove (alwaysAsId=true)
flag from the children
field. However, I want both endpoints coexist and render different layout. So, the question is: How can I make specific controller method choose whether full entities are replaced with ids?.
I already tried various combinations with @JsonView
, but it seems this approach doesn't work. @JsonView
can only control whether specific field is included or completely omitted, whereas I need children
field to only change layout. Also, since child type is same as entity type, there is no way to split annotation marks between different classes.
Upvotes: 1
Views: 653
Reputation: 2528
I finally found solution. It is based on first version of Antot's answer and uses custom serializer for content of children
collection. The tree producing endpoint is marked with @JsonView(Category.TreeView.class)
annotation and unmarked field inclusion is turned ON
#application.properties
spring.jackson.mapper.default-view-inclusion=true
// Category.java
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonSerialize(contentUsing = CategoryChildSerializer.class)
private Set<Category> children;
// CategoryChildSerializer.java
public class CategoryChildSerializer extends JsonSerializer<Category> implements ResolvableSerializer {
private JsonSerializer<Object> defaultSerializer;
private JsonSerializer<Object> idSerializer;
public void serialize(Category value, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException {
if(provider.getActiveView() == Category.TreeView.class)
defaultSerializer.serialize(value, gen, provider);
else
idSerializer.serialize(value.getId(), gen, provider);
}
public void resolve(SerializerProvider provider) throws JsonMappingException {
defaultSerializer = provider.findValueSerializer(Category.class);
idSerializer = provider.findValueSerializer(Integer.class);
}
}
Note how switching serializer delegates serialization instead of directly using JsonGenerator
. Implementing ResolvableSerializer
is in fact optional, it's just optimization step.
Upvotes: 0
Reputation: 3964
/* Updated answer, after the remark about loosing the view markers in multiple levels hierarchies. */
The result you are looking for can still be achieved by using @JsonView
s and a minor work-around inside Category
object.
Supposing we have two types used as markers to output JSON views FlatView
and HierarchicalView
.
The principle of the solution is:
we associate most of the fields with the views. children
field remains associated with FlatView
only.
we create an additional getter for the children
field, providing it with another property name, not clashing with the field or its original getter. This property is associated with HierarchicalView
only.
It gives the following layout for Category
class:
public class Category {
@JsonView({ FlatView.class, HierarchicalView.class}) // both views
private Integer id;
@JsonView({ FlatView.class, HierarchicalView.class}) // both views
private String displayName;
@JsonView({ FlatView.class, HierarchicalView.class}) // both views
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonIdentityReference(alwaysAsId = true)
private Category parent;
@JsonView(FlatView.class) // flat view only!
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonIdentityReference(alwaysAsId = true)
private Set<Category> children;
@JsonView(HierarchicalView.class) // hierarchical view only!
// note that the name is not `getChildren`: this generates another JSON property.
// Or use @JsonProperty to customize it.
public Set<Category> getChildrenAsTree() {
return this.children;
}
// constructors, getters and setters
}
The controller methods producing the output should be annotated with respective @JsonView
s.
Drawbacks:
This approach does not use the same property name for same information represented differently. This might cause some difficulties for property matching, deserialization at client side.
If the default view is still used somewhere for Category
, it will contain both the field and the property accessed via the additional getter.
Redundant and repetitive annotations :) harder to maintain.
Redundant getter for children
field.
Upvotes: 1