The
content
object(s) exposed in resources are JPA Entities. The interesting point about wrapping a JPA Entity in a resource comes with the low-level nature of an Entity itself, which supposedly represents a restricted identifiable domain area. This definition should ideally be entirely translated to the exposed REST resources.
So, how do we represent an Entity in REST HATEOAS? How do we safely and uniformly represent the JPA associations?
This recipe presents a simple and conservative method to answer these questions.
Index.java
). Here is another entity that is used: Exchange.java
. This entity presents a similar strategy to expose its JPA associations:import edu.zc.csm.core.converters.IdentifiableSerializer; import edu.zc.csm.core.converters.IdentifiableToIdConverter; @Entity public class Exchange extends ProvidedId<String> { private String name; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "market_id", nullable=true) @JsonSerialize(using=IdentifiableSerializer.class) @JsonProperty("marketId") @XStreamConverter(value=IdentifiableToIdConverter.class, strings={"id"}) @XStreamAlias("marketId") private Market market; @OneToMany(mappedBy = "exchange", cascade = CascadeType.ALL, fetch=FetchType.LAZY) @JsonIgnore @XStreamOmitField private Set<Index> indices = new LinkedHashSet<>(); @OneToMany(mappedBy = "exchange", cascade = CascadeType.ALL, fetch=FetchType.LAZY) @JsonIgnore @XStreamOmitField private Set<StockProduct> stocks = new LinkedHashSet<>(); public Exchange(){} public Exchange(String exchange) { setId(exchange); } //getters & setters @Override public String toString() { return "Exchange [name=" + name + ", market=" + market + ", id=" + id+ "]"; } }
Exchange.java
Entity references two custom utility classes that are used to transform the way external Entities are fetched as part of the main entity rendering (JSON or XML). Those utility classes are the following IdentifiableSerializer
and the IdentifiableToIdConverter
:IdentifiableSerializer
class is used for JSON marshalling:import org.springframework.hateoas.Identifiable; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; public class IdentifiableSerializer extends JsonSerializer<Identifiable<?>> { @Override public void serialize(Identifiable<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { provider.defaultSerializeValue(value.getId(), jgen); } }
IdentifiableToIdConverter
class is used for XML marshlling and is built with XStream dependencies:import com.thoughtworks.xstream.converters.Converter; public class IdentifiableToIdConverter implements Converter { private final Class <Identifiable<?>> type; public IdentifiableToIdConverter(final Class <Identifiable<?>> type, final Mapper mapper, final ReflectionProvider reflectionProvider, final ConverterLookup lookup, final String valueFieldName) { this(type, mapper, reflectionProvider, lookup, valueFieldName, null); } public IdentifiableToIdConverter(final Class<Identifiable<?>> type, final Mapper mapper, final ReflectionProvider reflectionProvider, final ConverterLookup lookup, final String valueFieldName, Class valueDefinedIn) { this.type = type; Field field = null; try { field = (valueDefinedIn != null? valueDefinedIn : type.getSuperclass()).getDeclaredField("id"); if (!field.isAccessible()) { field.setAccessible(true); } } catch (NoSuchFieldException e) { throw new IllegalArgumentException( e.getMessage()+": "+valueFieldName); } } public boolean canConvert(final Class type) { return type.isAssignableFrom(this.type); } public void marshal(final Object source, final HierarchicalStreamWriter writer,final MarshallingContext context) { if(source instanceof Identifiable){ writer.setValue( ((Identifiable<?>)source).getId() .toString() ); } } public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) { return null; } }
Let's understand how this strategy works.
One REST architectural constraint is to present a uniform interface. A uniform interface is achieved by exposing resources from endpoints that can all be targeted from different HTTP methods (if applicable).
Resources can also be exposed under several representations (json
, xml
, and so on), and information or error messages must be self-descriptive. The implementation of HATEOAS provides a great bonus for the self-explanatory character of an API.
In REST, the more intuitive and inferable things are, the better. From this perspective, as a web/UI developer, I should be able to assume the following:
GET
call on an endpoint will be the expected structure that I have to send back with a PUT
call (the edition of the object)POST
method)This consistency of payload structures among different HTTP methods is a SOLID and conservative argument that is used when it is time to defend the API interests. It's pretty much always the time to defend the API interests.
Exposing the minimum amount of information has been the core idea during the refactoring for this chapter. It's usually a great way to ensure that one endpoint won't be used to expose information data that would be external to the initial controller.
A JPA Entity can have associations to other Entities (@OneToOne
, @OneToMany
, @ManyToOne
, or @ManyToMany
).
Some of these associations have been annotated with @JsonIgnore
(and @XStreamOmitField
), and some other associations have been annotated with @JsonSerialize
and @JsonProperty
(and @XStreamConverter
and @XStreamAlias
).
In this situation, the database table of the Entity doesn't have a foreign key to the table of the targeted second Entity.
The strategy here is to completely ignore the relationship in REST to reflect the database state.
The ignore
instructions depend on the supported representations and the chosen implementations.
For json
, we are using Jackson
, the solution has been: @JsonIgnore
.
For xml
, we are using XStream
, the solution has been: @XstreamOmitField
.
Here, the database table of the Entity has a foreign key the table of the targeted second Entity.
If we plan to update an entity of this table, which depends on an entity of the other table, we will have to provide this foreign key for the entity.
The idea then is to expose this foreign key as a dedicated field just as all the other columns of the database table. Again, the solution to implement this depends on the supported representations and the configured marshallers.
For json
and Jackson
, we have done it with the following code snippet:
@JsonSerialize(using=IdentifiableSerializer.class) @JsonProperty("marketId")
As you can see, we rename the attribute to suggest that we are presenting (and expecting) an ID. We have created the IdentifiableSerializer
class that extracts the ID
from the entity (from the Identifiable
interface) and places only this ID
into the value of the attribute.
For xml
and XStream
, it has been:
@XStreamConverter(value=IdentifiableToIdConverter.class, strings={"id"}) @XStreamAlias("marketId")
In the same way, we rename the attribute to suggest that we are presenting an ID
, and we target the custom converter IdentifiableToIdConverter
that also chooses only the ID of the Entity as a value for the attribute.
Here is an example of the xml
representation example of the ^AMBAPT
index:
This strategy promotes a clear separation between resources. The displayed fields for each resource match the database schema entirely. This is a standard practice in web developments to keep the HTTP request payload unchanged for the different HTTP methods.
When HATEOAS is adopted, we should then fully encourage the use of links to access related entities instead of nested views.
The previous recipe Building links for a Hypermedia-Driven API features examples to access (using links) the Entities that are associated with @...ToOne
and @...ToMany
. Below is an example of these links in an exposed Entity as it is achieved in the previous recipe:
We detail here official sources of information for the implemented marshallers.
XStream has been migrated from codehaus.org to Github. To follow an official tutorial about XStream converters, go to: