Barbadoss
Barbadoss

Reputation: 1156

Spring: one JPA model, many JSON respresentations

I'm writing a RESTful web service using Spring/JPA. There's a JPA model which is exposed through the web service. The 'Course' model is quite spacious - it actually is composed of several sets of data: general information, pricing details and some caches.

The issue I encounter is the inability to issue different JSON representations using the same JPA model.

The in first case I only need to return general_info set of data for courses:

GET /api/courses/general_info

in the second case I would like to return pricing set of data only:

GET /api/courses/pricing

I see the following ways to solve this, not in particular order:

  1. To create CourseGeneralInfo and CoursePricing JPA models using the origin database table as a source. CourseGeneralInfo model would have its own set of fields and CoursePricing would have its own ones. This way I would have the JSON I need.

  2. To refactor the stuff out of the Course model/table to have GeneralInfo and PricingDetails to be separate JPA entities. Ok, this sounds like the best one (imo) though the database is legacy and it is not something I can change easily...

  3. Leverage some sort of DTO and Spring Mappers to convert the JPA model to representation needed in any particular case.

What approach would you recommend?

Upvotes: 1

Views: 2062

Answers (2)

mavarazy
mavarazy

Reputation: 7735

In Spring Data REST 2.1 there is a new mechanism for this purpose - Projections (It's now part of spring-data-commons).

You'll need to define interface, containing exactly exposed fields:

@Projection(name = "summary", types = Course.class)
interface CourseGeneralInfo {

  GeneralInfo getInfo();

}

After that Spring will be able to find it automagically in your source, and you could make requests to your existing endpoints, like this:

GET /api/courses?projection=general_info

Based on https://spring.io/blog/2014/05/21/what-s-new-in-spring-data-dijkstra

Spring sample project with projections: https://github.com/spring-projects/spring-data-examples/tree/master/rest/projections

Upvotes: 1

PaulProgrammer
PaulProgrammer

Reputation: 17710

I was just reading about some really nifty features in Spring 4.1, which allow you to use different views via annotations.

from: https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring

public class View {
    interface Summary {}
}

public class User {

    @JsonView(View.Summary.class)
    private Long id;

    @JsonView(View.Summary.class)
    private String firstname;

    @JsonView(View.Summary.class)
    private String lastname;

    private String email;
    private String address;
    private String postalCode;
    private String city;
    private String country;
}

public class Message {

    @JsonView(View.Summary.class)
    private Long id;

    @JsonView(View.Summary.class)
    private LocalDate created;

    @JsonView(View.Summary.class)
    private String title;

    @JsonView(View.Summary.class)
    private User author;

    private List<User> recipients;

    private String body;
}

Thanks to Spring MVC @JsonView support, it is possible to choose, on a per handler method basis, which field should be serialized:

@RestController
public class MessageController {

    @Autowired
    private MessageService messageService;

    @JsonView(View.Summary.class)
    @RequestMapping("/")
    public List<Message> getAllMessages() {
        return messageService.getAll();
    }

    @RequestMapping("/{id}")
    public Message getMessage(@PathVariable Long id) {
        return messageService.get(id);
    }
}

In this example, if all messages are retrieved, only the most important fields are serialized thanks to the getAllMessages() method annotated with @JsonView(View.Summary.class):

[ {
  "id" : 1,
  "created" : "2014-11-14",
  "title" : "Info",
  "author" : {
    "id" : 1,
    "firstname" : "Brian",
    "lastname" : "Clozel"
  }
}, {
  "id" : 2,
  "created" : "2014-11-14",
  "title" : "Warning",
  "author" : {
    "id" : 2,
    "firstname" : "Stéphane",
    "lastname" : "Nicoll"
  }
}, {
  "id" : 3,
  "created" : "2014-11-14",
  "title" : "Alert",
  "author" : {
    "id" : 3,
    "firstname" : "Rossen",
    "lastname" : "Stoyanchev"
  }
} ]

In Spring MVC default configuration, MapperFeature.DEFAULT_VIEW_INCLUSION is set to false. That means that when enabling a JSON View, non annotated fields or properties like body or recipients are not serialized.

When a specific Message is retrieved using the getMessage() handler method (no JSON View specified), all fields are serialized as expected:

{
  "id" : 1,
  "created" : "2014-11-14",
  "title" : "Info",
  "body" : "This is an information message",
  "author" : {
    "id" : 1,
    "firstname" : "Brian",
    "lastname" : "Clozel",
    "email" : "[email protected]",
    "address" : "1 Jaures street",
    "postalCode" : "69003",
    "city" : "Lyon",
    "country" : "France"
  },
  "recipients" : [ {
    "id" : 2,
    "firstname" : "Stéphane",
    "lastname" : "Nicoll",
    "email" : "[email protected]",
    "address" : "42 Obama street",
    "postalCode" : "1000",
    "city" : "Brussel",
    "country" : "Belgium"
  }, {
    "id" : 3,
    "firstname" : "Rossen",
    "lastname" : "Stoyanchev",
    "email" : "[email protected]",
    "address" : "3 Warren street",
    "postalCode" : "10011",
    "city" : "New York",
    "country" : "USA"
  } ]
}

Only one class or interface can be specified with the @JsonView annotation, but you can use inheritance to represent JSON View hierarchies (if a field is part of a JSON View, it will be also part of parent view). For example, this handler method will serialize fields annotated with @JsonView(View.Summary.class) and @JsonView(View.SummaryWithRecipients.class):

public class View {
    interface Summary {}
    interface SummaryWithRecipients extends Summary {}
}

public class Message {

    @JsonView(View.Summary.class)
    private Long id;

    @JsonView(View.Summary.class)
    private LocalDate created;

    @JsonView(View.Summary.class)
    private String title;

    @JsonView(View.Summary.class)
    private User author;

    @JsonView(View.SummaryWithRecipients.class)
    private List<User> recipients;

    private String body;
}

@RestController
public class MessageController {

    @Autowired
    private MessageService messageService;

    @JsonView(View.SummaryWithRecipients.class)
    @RequestMapping("/with-recipients")
    public List<Message> getAllMessagesWithRecipients() {
        return messageService.getAll();
    }
}

Upvotes: 3

Related Questions