Projections and Excerpts

Spring Data REST presents a default view of the domain model you export. However, sometimes, you may need to alter the view of that model for various reasons. This section covers how to define projections and excerpts to serve up simplified and reduced views of resources.

Projections

Consider the following domain model:

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
  …
}

The Person object in the preceding example has several attributes:

  • id is the primary key.

  • firstName and lastName are data attributes.

  • address is a link to another domain object.

Now assume that we create a corresponding repository, as follows:

interface PersonRepository extends CrudRepository<Person, Long> {}

By default, Spring Data REST exports this domain object, including all of its attributes. firstName and lastName are exported as the plain data objects that they are. There are two options regarding the address attribute. One option is to also define a repository for Address objects, as follows:

interface AddressRepository extends CrudRepository<Address, Long> {}

In this situation, a Person resource renders the address attribute as a URI to its corresponding Address resource. If we were to look up “Frodo” in the system, we could expect to see a HAL document like this:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    },
    "address" : {
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}

There is another way. If the Address domain object does not have its own repository definition, Spring Data REST includes the data fields inside the Person resource, as the following example shows:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : {
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    }
  }
}

But what if you do not want address details at all? Again, by default, Spring Data REST exports all of its attributes (except the id). You can offer the consumer of your REST service an alternative by defining one or more projections. The following example shows a projection that does not include the address:

@Projection(name = "noAddresses", types = { Person.class }) (1)
interface NoAddresses { (2)

  String getFirstName(); (3)

  String getLastName(); (4)
}
1 The @Projection annotation flags this as a projection. The name attribute provides the name of the projection, which we cover in more detail shortly. The types attributes targets this projection to apply only to Person objects.
2 It is a Java interface, making it declarative.
3 It exports the firstName.
4 It exports the lastName.

The NoAddresses projection only has getters for firstName and lastName, meaning that it does not serve up any address information. Assuming you have a separate repository for Address resources, the default view from Spring Data REST differs slightly from the previous representation, as the following example shows:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1{?projection}", (1)
      "templated" : true (2)
    },
    "address" : {
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}
1 This resource has a new option: {?projection}.
2 The self URI is a URI Template.

To view the projection to the resource, look up localhost:8080/persons/1?projection=noAddresses.

The value supplied to the projection query parameter is the same as that specified in @Projection(name = "noAddress"). It has nothing to do with the name of the projection’s interface.

You can have multiple projections.

See Projections to see an example project. We encourage you to experiment with it.

Spring Data REST finds projection definitions as follows:

  • Any @Projection interface found in the same package as your entity definitions (or one of its sub-packages) is registered.

  • You can manually register a projection by using RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…).

In either case, the projection interface must have the @Projection annotation.

Finding Existing Projections

Spring Data REST exposes Application-Level Profile Semantics (ALPS) documents, a micro metadata format. To view the ALPS metadata, follow the profile link exposed by the root resource. If you navigate down to the ALPS document for Person resources (which would be /alps/persons), you can find many details about Person resources. Projections are listed, along with the details about the GET REST transition, in blocks similar to the following example:

{ …
  "id" : "get-person", (1)
  "name" : "person",
  "type" : "SAFE",
  "rt" : "#person-representation",
  "descriptors" : [ {
    "name" : "projection", (2)
    "doc" : {
      "value" : "The projection that shall be applied when rendering the response. Acceptable values available in nested descriptors.",
      "format" : "TEXT"
    },
    "type" : "SEMANTIC",
    "descriptors" : [ {
      "name" : "noAddresses", (3)
      "type" : "SEMANTIC",
      "descriptors" : [ {
        "name" : "firstName", (4)
        "type" : "SEMANTIC"
      }, {
        "name" : "lastName", (4)
        "type" : "SEMANTIC"
      } ]
    } ]
  } ]
},
…
1 This part of the ALPS document shows details about GET and Person resources.
2 This part contais the projection options.
3 This part contains the noAddresses projection.
4 The actual attributes served up by this projection include firstName and lastName.

Projection definitions are picked up and made available for clients if they are:

  • Flagged with the @Projection annotation and located in the same package (or sub-package) of the domain type, OR

  • Manually registered by using RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…).

Bringing in Hidden Data

So far in this section, we have covered how projections can be used to reduce the information that is presented to the user. Projections can also bring in normally unseen data. For example, Spring Data REST ignores fields or getters that are marked up with @JsonIgnore annotations. Consider the following domain object:

@Entity
public class User {

	@Id @GeneratedValue
	private Long id;
	private String name;

	@JsonIgnore private String password; (1)

	private String[] roles;
  …
1 Jackson’s @JsonIgnore is used to prevent the password field from being serialized into JSON.

The User class in the preceding example can be used to store user information as well as integration with Spring Security. If you create a UserRepository, the password field would normally have been exported, which is not good. In the preceding example, we prevent that from happening by applying Jackson’s @JsonIgnore on the password field.

Jackson also does not serialize the field into JSON if @JsonIgnore is on the field’s corresponding getter function.

However, projections introduce the ability to still serve this field. It is possible to create the following projection:

@Projection(name = "passwords", types = { User.class })
interface PasswordProjection {

  String getPassword();
}

If such a projection is created and used, it sidesteps the @JsonIgnore directive placed on User.password.

This example may seem a bit contrived, but it is possible, with a richer domain model and many projections, to accidentally leak such details. Since Spring Data REST cannot discern the sensitivity of such data, it is up to you to avoid such situations.

Projections can also generate virtual data. Imagine you had the following entity definition:

@Entity
public class Person {

  ...
  private String firstName;
  private String lastName;

  ...
}

You can create a projection that combines the two data fields in the preceding example together, as follows:

@Projection(name = "virtual", types = { Person.class })
public interface VirtualProjection {

  @Value("#{target.firstName} #{target.lastName}") (1)
  String getFullName();

}
1 Spring’s @Value annotation lets you plug in a SpEL expression that takes the target object and splices together its firstName and lastName attributes to render a read-only fullName.

Excerpts

An excerpt is a projection that is automatically applied to a resource collection. For example, you can alter the PersonRepository as follows:

@RepositoryRestResource(excerptProjection = NoAddresses.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

The preceding example directs Spring Data REST to use the NoAddresses projection when embedding Person resources into collections or related resources.

Excerpt projections are not automatically applied to single resources. They have to be applied deliberately. Excerpt projections are meant to provide a default preview of collection data but not when fetching individual resources. See Why is an excerpt projection not applied automatically for a Spring Data REST item resource? for a discussion on the subject.

In addition to altering the default rendering, excerpts have additional rendering options as shown in the next section.

Excerpting Commonly Accessed Data

A common situation with REST services arises when you compose domain objects. For example, a Person is stored in one table and their related Address is stored in another. By default, Spring Data REST serves up the person’s address as a URI the client must navigate. But if it is common for consumers to always fetch this extra piece of data, an excerpt projection can put this extra piece of data inline, saving you an extra GET. To do so, you can define another excerpt projection, as follows:

@Projection(name = "inlineAddress", types = { Person.class }) (1)
interface InlineAddress {

  String getFirstName();

  String getLastName();

  Address getAddress(); (2)
}
1 This projection has been named inlineAddress.
2 This projection adds getAddress, which returns the Address field. When used inside a projection, it causes the information to be included inline.

You can plug it into the PersonRepository definition, as follows:

@RepositoryRestResource(excerptProjection = InlineAddress.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

Doing so causes the HAL document to appear as follows:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : { (1)
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1"
    },
    "address" : { (2)
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}
1 The address data is directly included inline, so you do not have to navigate to get it.
2 The link to the Address resource is still provided, making it still possible to navigate to its own resource.

Note that the preceding example is a mix of the examples shown earlier in this chapter. You may want to read back through them to follow the progression to the final example.

Configuring @RepositoryRestResource(excerptProjection=…​) for a repository alters the default behavior. This can potentially cause breaking changes to consumers of your service if you have already made a release.