Vasily Liaskovsky
Vasily Liaskovsky

Reputation: 2528

Jackson with Spring boot: control object substitution for specific method

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

Answers (2)

Vasily Liaskovsky
Vasily Liaskovsky

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

Antot
Antot

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 @JsonViews 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 @JsonViews.

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

Related Questions