4. Media types
4.1. HAL – Hypertext Application Language
JSON Hypertext Application Language or HAL is one of the simplest and most widely adopted hypermedia media types adopted when not discussing specific web stacks.
It was the first spec-based media type adopted by Spring HATEOAS.
4.1.1. Building HAL representation models
As of Spring HATEOAS 1.1, we ship a dedicated HalModelBuilder
that allows to create RepresentationModel
instances through a HAL-idiomatic API.
These are its fundamental assumptions:
-
A HAL representation can be backed by an arbitrary object (an entity) that builds up the domain fields contained in the representation.
-
The representation can be enriched by a variety of embedded documents, which can be either arbitrary objects or HAL representations themselves (i.e. containing nested embeddeds and links).
-
Certain HAL specific patterns (e.g. previews) can be directly used in the API so that the code setting up the representation reads like you’d describe a HAL representation following those idioms.
Here’s an example of the API used:
// An order
var order = new Order(…); (1)
// The customer who placed the order
var customer = customer.findById(order.getCustomerId());
var customerLink = Link.of("/orders/{id}/customer") (2)
.expand(order.getId())
.withRel("customer");
var additional = …
var model = HalModelBuilder.halModelOf(order)
.preview(new CustomerSummary(customer)) (3)
.forLink(customerLink) (4)
.embed(additional) (5)
.link(Link.of(…, IanaLinkRelations.SELF));
.build();
1 | We set up some domain type. In this case, an order that has a relationship to the customer that placed it. |
2 | We prepare a link pointing to a resource that will expose customer details |
3 | We start building a preview by providing the payload that’s supposed to be rendered inside the _embeddable clause. |
4 | We conclude that preview by providing the target link. It transparently gets added to the _links object and its link relation is used as the key for the object provided in the previous step. |
5 | Other objects can be added to show up under _embedded .
The key under which they’re listed is derived from the objects relation settings. They’re customizable via @Relation or a dedicated LinkRelationProvider (see Using the LinkRelationProvider API for details). |
{
"_links" : {
"self" : { "href" : "…" }, (1)
"customer" : { "href" : "/orders/4711/customer" } (2)
},
"_embedded" : {
"customer" : { … }, (3)
"additional" : { … } (4)
}
}
1 | The self link as explicitly provided. |
2 | The customer link transparently added through ….preview(…).forLink(…) . |
3 | The preview object provided. |
4 | Additional elements added via explicit ….embed(…) . |
In HAL _embedded
is also used to represent top collections.
They’re usually grouped under the link relation derived from the object’s type.
I.e. a list of orders would look like this in HAL:
{
"_embedded" : {
"order : [
… (1)
]
}
}
1 | Individual order documents go here. |
Creating such a representation is as easy as this:
Collection<Order> orders = …;
HalModelBuilder.emptyHalDocument()
.embed(orders);
That said, if the order is empty, there’s no way to derive the link relation to appear inside _embedded
, so that the document will stay empty if the collection is empty.
If you prefer to explicitly communicate an empty collection, a type can be handed into the overload of the ….embed(…)
method taking a Collection
.
If the collection handed into the method is empty, this will cause a field rendered with its link relation derived from the given type.
HalModelBuilder.emptyHalModel()
.embed(Collections.emptyList(), Order.class);
// or
.embed(Collections.emptyList(), LinkRelation.of("orders"));
will create the following, more explicit representation.
{
"_embedded" : {
"orders" : []
}
}
4.1.2. Configuring link rendering
In HAL, the _links
entry is a JSON object. The property names are link relations and
each value is either a link object or an array of link objects.
For a given link relation that has two or more links, the spec is clear on representation:
{
"_links": {
"item": [
{ "href": "https://myhost/cart/42" },
{ "href": "https://myhost/inventory/12" }
]
},
"customer": "Dave Matthews"
}
But if there is only one link for a given relation, the spec is ambiguous. You could render that as either a single object or as a single-item array.
By default, Spring HATEOAS uses the most terse approach and renders a single-link relation like this:
{
"_links": {
"item": { "href": "https://myhost/inventory/12" }
},
"customer": "Dave Matthews"
}
Some users prefer to not switch between arrays and objects when consuming HAL. They would prefer this type of rendering:
{
"_links": {
"item": [{ "href": "https://myhost/inventory/12" }]
},
"customer": "Dave Matthews"
}
If you wish to customize this policy, all you have to do is inject a HalConfiguration
bean into your application configuration.
There are multiple choices.
@Bean
public HalConfiguration globalPolicy() {
return new HalConfiguration() //
.withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 | Override Spring HATEOAS’s default by rendering ALL single-link relations as arrays. |
If you prefer to only override some particular link relations, you can create a HalConfiguration
bean like this:
@Bean
public HalConfiguration linkRelationBasedPolicy() {
return new HalConfiguration() //
.withRenderSingleLinksFor( //
IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
.withRenderSingleLinksFor( //
LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1 | Always render item link relations as an array. |
2 | Render prev link relations as an object when there is only one link. |
If neither of these match your needs, you can use an Ant-style path pattern:
@Bean
public HalConfiguration patternBasedPolicy() {
return new HalConfiguration() //
.withRenderSingleLinksFor( //
"http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 | Render all link relations that start with http as an array. |
The pattern-based approach uses Spring’s AntPathMatcher .
|
All of these HalConfiguration
withers can be combined to form one comprehensive policy. Be sure to test your API
extensively to avoid surprises.
4.1.3. Link title internationalization
HAL defines a title
attribute for its link objects.
These titles can be populated by using Spring’s resource bundle abstraction and a resource bundle named rest-messages
so that clients can use them in their UIs directly.
This bundle will be set up automatically and is used during HAL link serialization.
To define a title for a link, use the key template _links.$relationName.title
as follows:
rest-messages.properties
_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout
This will result in the following HAL representation:
{
"_links" : {
"cancel" : {
"href" : "…"
"title" : "Cancel order"
},
"payment" : {
"href" : "…"
"title" : "Proceed to checkout"
}
}
}
4.1.4. Using the CurieProvider
API
The Web Linking RFC describes registered and extension link relation types. Registered rels are well-known strings registered with the IANA registry of link relation types. Extension rel
URIs can be used by applications that do not wish to register a relation type. Each one is a URI that uniquely identifies the relation type. The rel
URI can be serialized as a compact URI or Curie. For example, a curie of ex:persons
stands for the link relation type example.com/rels/persons
if ex
is defined as example.com/rels/{rel}
. If curies are used, the base URI must be present in the response scope.
The rel
values created by the default RelProvider
are extension relation types and, as a result, must be URIs, which can cause a lot of overhead. The CurieProvider
API takes care of that: It lets you define a base URI as a URI template and a prefix that stands for that base URI. If a CurieProvider
is present, the RelProvider
prepends all rel
values with the curie prefix. Furthermore a curies
link is automatically added to the HAL resource.
The following configuration defines a default curie provider:
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {
@Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}"));
}
}
Note that now the ex:
prefix automatically appears before all rel values that are not registered with IANA, as in ex:orders
. Clients can use the curies
link to resolve a curie to its full form.
The following example shows how to do so:
{
"_links": {
"self": {
"href": "https://myhost/person/1"
},
"curies": {
"name": "ex",
"href": "https://example.com/rels/{rel}",
"templated": true
},
"ex:orders": {
"href": "https://myhost/person/1/orders"
}
},
"firstname": "Dave",
"lastname": "Matthews"
}
Since the purpose of the CurieProvider
API is to allow for automatic curie creation, you can define only one CurieProvider
bean per application scope.
4.2. HAL-FORMS
HAL-FORMS is designed to add runtime FORM support to the HAL media type.
HAL-FORMS "looks like HAL." However, it is important to keep in mind that HAL-FORMS is not the same as HAL — the two should not be thought of as interchangeable in any way.
HAL-FORMS spec
To enable this media type, put the following configuration in your code:
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {
}
Anytime a client supplies an Accept
header with application/prs.hal-forms+json
, you can expect something like this:
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/employees/1"
}
},
"_templates" : {
"default" : {
"method" : "put",
"properties" : [ {
"name" : "firstName",
"required" : true
}, {
"name" : "lastName",
"required" : true
}, {
"name" : "role",
"required" : true
} ]
},
"partiallyUpdateEmployee" : {
"method" : "patch",
"properties" : [ {
"name" : "firstName",
"required" : false
}, {
"name" : "lastName",
"required" : false
}, {
"name" : "role",
"required" : false
} ]
}
}
}
Check out the HAL-FORMS spec to understand the details of the _templates attribute. Read about the Affordances API to augment your controllers with this extra metadata.
As for single-item (EntityModel
) and aggregate root collections (CollectionModel
), Spring HATEOAS renders them
identically to HAL documents.
4.2.1. Defining HAL-FORMS metadata
HAL-FORMS allows to describe criterias for each form field. Spring HATEOAS allows to customize those by shaping the model type for the input and output types and using annotations on them.
Each template will get the following attributes defined:
Attribute | Description |
---|---|
|
The media type expected to be received by the server. Only included if the controller method pointed to exposes a |
|
The HTTP method to use when submitting the template. |
|
The target URI to submit the form to. Will only be rendered if the affordance target is different than the link it was declared on. |
|
The human readable title when displaying the template. |
|
All properties to be submitted with the form (see below). |
Each property will get the following attributes defined:
Attribute | Description |
---|---|
|
Set to |
|
Can be customized by using JSR-303’s |
|
Can be customized by using JSR-303’s |
|
The maximum value allowed for the property. Derived from Derived from JSR-303’s |
|
The maximum length value allowed for the property. Derived from Hibernate Validator’s |
|
The minimum value allowed for the property. Derived from JSR-303’s |
|
The minimum length value allowed for the property. Derived from Hibernate Validator’s |
|
The options to select a value from when submitting the form. For details, see Defining HAL-FORMS options for a property. |
|
The user readable prompt to use when rendering the form input. For details, see Property prompts. |
|
A user readable placeholder, to give an example for a format expected. The way of defining those follows Property prompts but uses the suffix |
|
The HTML input type derived from the explicit |
For types that you cannot annotate manually, you can register a custom pattern via a HalFormsConfiguration
bean present in the application context.
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
}
}
This setup will cause the HAL-FORMS template properties for representation model properties of type CreditCardNumber
to declare a regex
field with value [0-9]{16}
.
Defining HAL-FORMS options for a property
For properties whose value is supposed to match a certain superset of values, HAL-FORMS defines the options
sub-document within a property definition.
Options available for a certain property can be described via HalFormsConfiguration
's withOptions(…)
taking a pointer to a type’s property and a creator function to turn a PropertyMetadata
into a HalFormsOptions
instance.
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.withOptions(Order.class, "shippingMethod" metadata ->
HalFormsOptions.inline("FedEx", "DHL"));
}
}
See how we set up the option values FedEx
and DHL
as the options to select from for the Order.shippingMethod
property.
Alternatively, HalFormsOptions.remote(…)
can point to a remote resource providing values dynamically.
Fore more constraints on options settings, refer to the spec or the Javadoc of HalFormsOptions
.
4.2.2. Internationalization of form attributes
HAL-FORMS contains attributes that are intended for human interpretation, like a template’s title or property prompts.
These can be defined and internationalized using Spring’s resource bundle support and the rest-messages
resource bundle configured by Spring HATEOAS by default.
Template titles
To define a template title use the following pattern: _templates.$affordanceName.title
. Note that in HAL-FORMS, the name of a template is default
if it is the only one.
This means that you’ll usually have to qualify the key with the local or fully qualified input type name that affordance describes.
_templates.default.title=Some title (1)
_templates.putEmployee.title=Create employee (2)
Employee._templates.default.title=Create employee (3)
com.acme.Employee._templates.default.title=Create employee (4)
1 | A global definition for the title using default as key. |
2 | A global definition for the title using the actual affordance name as key. Unless defined explicitly when creating the affordance, this defaults to the name of the method that has been pointed to when creating the affordance. |
3 | A locally defined title to be applied to all types named Employee . |
4 | A title definition using the fully-qualified type name. |
Keys using the actual affordance name enjoy preference over the defaulted ones. |
Property prompts
Property prompts can also be resolved via the rest-messages
resource bundle automatically configured by Spring HATEOAS.
The keys can be defined globally, locally or fully-qualified and need an ._prompt
concatenated to the actual property key:
email
propertyfirstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1 | All properties named firstName will get "Firstname" rendered, independent of the type they’re declared in. |
2 | The firstName property in types named Employee will be prompted "Firstname". |
3 | The firstName property of com.acme.Employee will get a prompt of "Firstname" assigned. |
4.2.3. A complete example
Let’s have a look at some example code that combines all the definition and customization attributes described above.
A RepresentationModel
for a customer might look something like this:
class CustomerRepresentation
extends RepresentationModel<CustomerRepresentation> {
String name;
LocalDate birthdate; (1)
@Pattern(regex = "[0-9]{16}") String ccn; (2)
@Email String email; (3)
}
1 | We define a birthdate property of type LocalDate . |
2 | We expect ccn to adhere to a regular expression. |
3 | We define email to be an email using the JSR-303 @Email annotation. |
Note that this type is not a domain type. It’s intentionally designed to capture a wide range of potentially invalid input so that potentially erroneous valies for the fields can be rejected at once.
Let’s continue by having a look at how a controller makes use of that model:
@Controller
class CustomerController {
@PostMapping("/customers")
EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { (1)
// …
}
@GetMapping("/customers")
CollectionModel<?> getCustomers() {
CollectionModel<?> model = …;
CustomerController controller = methodOn(CustomerController.class);
model.add(linkTo(controller.getCustomers()).withSelfRel() (2)
.andAfford(controller.createCustomer(null)));
return ResponseEntity.ok(model);
}
}
1 | A controller method is declared to use the representation model defined above to bind the request body to if a POST is issued to /customers . |
2 | A GET request to /customers prepares a model, adds a self link to it and additionally declares an affordance on that very link pointing to the controller method mapped to POST .
This will cause an affordance model to be built up, which — depending on the media type to be rendered eventually — will be translated into the media type specific format. |
Next, let’s add some additional metadata to make the form more accessible to humans:
rest-messages.properties
.CustomerRepresentation._template.createCustomer.title=Create customer (1)
CustomerRepresentation.ccn._prompt=Credit card number (2)
CustomerRepresentation.ccn._placeholder=1234123412341234 (2)
1 | We define an explicit title for the template created by pointing to the createCustomer(…) method. |
2 | We explicitly a prompt and placeholder for the ccn property of the CustomerRepresentation model. |
If a client now issues a GET
request to /customers
using an Accept
header of application/prs.hal-forms+json
, the response HAL document is extended to a HAL-FORMS one to include the following _templates
definition:
{
…,
"_templates" : {
"default" : { (1)
"title" : "Create customer", (2)
"method" : "post", (3)
"properties" : [ {
"name" : "name",
"required" : true,
"type" : "text" (4)
} , {
"name" : "birthdate",
"required" : true,
"type" : "date" (4)
} , {
"name" : "ccn",
"prompt" : "Credit card number", (5)
"placeholder" : "1234123412341234" (5)
"required" : true,
"regex" : "[0-9]{16}", (6)
"type" : "text"
} , {
"name" : "email",
"prompt" : "Email",
"required" : true,
"type" : "email" (7)
} ]
}
}
}
1 | A template named default is exposed. Its name is default as it’s the sole template defined and the spec requires that name to be used.
If multiple templates are attached (by declaring additional affordances) they will be each named after the method they’re pointing to. |
2 | The template title is derived from the value defined in the resource bundle. Note, that depending on the Accept-Language header sent with the request and the availability different values might returned. |
3 | The method attribute’s value is derived from the mapping of the method the affordance was derived from. |
4 | The type attribute’s value text is derived from the property’s type String .
The same applies to birthdate property, but resulting in date . |
5 | The prompt and placeholder for the ccn property are derived from the resource bundle as well. |
6 | The @Pattern declaration for the ccn property is exposed as regex attribute of the template property. |
7 | The @Email annotation on the email property has been translated into the corresponding type value. |
HAL-FORMS templates are considered by e.g. the HAL Explorer, which automatically renders HTML forms from those descriptions.
4.3. HTTP Problem Details
Problem Details for HTTP APIs is a media type to carry machine-readable details of errors in a HTTP response to avoid the need to define new error response formats for HTTP APIs.
HTTP Problem Details defines a set of JSON properties that carry additional information to describe error details to HTTP clients. Find more details about those properties in particular in the relevant section of the RFC document.
You can create such a JSON response by using the Problem
media type domain type in your Spring MVC Controller:
Problem
type@RestController
class PaymentController {
@PutMapping
ResponseEntity<?> issuePayment(@RequestBody PaymentRequest request) {
PaymentResult result = payments.issuePayment(request.orderId, request.amount);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
}
String title = messages.getMessage("payment.out-of-credit");
String detail = messages.getMessage("payment.out-of-credit.details", //
new Object[] { result.getBalance(), result.getCost() });
Problem problem = Problem.create() (1)
.withType(OUT_OF_CREDIT_URI) //
.withTitle(title) (2)
.withDetail(detail) //
.withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) //
.withProperties(map -> { (3)
map.put("balance", result.getBalance());
map.put("accounts", Arrays.asList( //
ACCOUNTS.expand(result.getSourceAccountId()), //
ACCOUNTS.expand(result.getTargetAccountId()) //
));
});
return ResponseEntity.status(HttpStatus.FORBIDDEN) //
.body(problem);
}
}
1 | You start by creating an instance of Problem using the factory methods exposed. |
2 | You can define the values for the default properties defined by the media type, e.g. the type URI, the title and details using internationalization features of Spring (see above). |
3 | Custom properties can be added via a Map or an explicit object (see below). |
To use a dedicated object for custom properties, declare a type, create and populate an instance of it and hand this into the Problem
instance either via ….withProperties(…)
or on instance creation via Problem.create(…)
.
class AccountDetails {
int balance;
List<URI> accounts;
}
problem.withProperties(result.getDetails());
// or
Problem.create(result.getDetails());
This will result in a response looking like this:
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
4.4. Collection+JSON
Collection+JSON is a JSON spec registered with IANA-approved media type application/vnd.collection+json
.
Collection+JSON is a JSON-based read/write hypermedia-type designed to support management and querying of simple collections.
Collection+JSON spec
Collection+JSON provides a uniform way to represent both single item resources as well as collections. To enable this media type, put the following configuration in your code:
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {
}
This configuration will make your application respond to requests that have an Accept
header of application/vnd.collection+json
as shown below.
The following example from the spec shows a single item:
{
"collection": {
"version": "1.0",
"href": "https://example.org/friends/", (1)
"links": [ (2)
{
"rel": "feed",
"href": "https://example.org/friends/rss"
},
{
"rel": "queries",
"href": "https://example.org/friends/?queries"
},
{
"rel": "template",
"href": "https://example.org/friends/?template"
}
],
"items": [ (3)
{
"href": "https://example.org/friends/jdoe",
"data": [ (4)
{
"name": "fullname",
"value": "J. Doe",
"prompt": "Full Name"
},
{
"name": "email",
"value": "[email protected]",
"prompt": "Email"
}
],
"links": [ (5)
{
"rel": "blog",
"href": "https://examples.org/blogs/jdoe",
"prompt": "Blog"
},
{
"rel": "avatar",
"href": "https://examples.org/images/jdoe",
"prompt": "Avatar",
"render": "image"
}
]
}
]
}
}
1 | The self link is stored in the document’s href attribute. |
2 | The document’s top links section contains collection-level links (minus the self link). |
3 | The items section contains a collection of data. Since this is a single-item document, it only has one entry. |
4 | The data section contains actual content. It’s made up of properties. |
5 | The item’s individual links . |
The previous fragment was lifted from the spec. When Spring HATEOAS renders an
|
When rendering a collection of resources, the document is almost the same, except there will be multiple entries inside
the items
JSON array, one for each entry.
Spring HATEOAS more specifically will:
-
Put the entire collection’s
self
link into the top-levelhref
attribute. -
The
CollectionModel
links (minusself
) will be put into the top-levellinks
. -
Each item-level
href
will contain the correspondingself
link for each entry from theCollectionModel.content
collection. -
Each item-level
links
will contain all other links for each entry fromCollectionModel.content
.
4.5. UBER - Uniform Basis for Exchanging Representations
UBER is an experimental JSON spec
The UBER document format is a minimal read/write hypermedia type designed to support simple state transfers and ad-hoc hypermedia-based transitions.
UBER spec
UBER provides a uniform way to represent both single item resources as well as collections. To enable this media type, put the following configuration in your code:
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {
}
This configuration will make your application respond to requests using the Accept
header application/vnd.amundsen-uber+json
as show below:
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "/employees/1"
}, {
"name" : "employee",
"data" : [ {
"name" : "role",
"value" : "ring bearer"
}, {
"name" : "name",
"value" : "Frodo"
} ]
} ]
}
}
This media type is still under development as is the spec itself. Feel free to open a ticket if you run into issues using it.
UBER media type is not associated in any way with Uber Technologies Inc., the ride sharing company. |
4.6. ALPS - Application-Level Profile Semantics
ALPS is a media type for providing profile-based metadata about another resource.
An ALPS document can be used as a profile to explain the application semantics of a document with an application- agnostic media type (such as HTML, HAL, Collection+JSON, Siren, etc.). This increases the reusability of profile documents across media types.
ALPS spec
ALPS requires no special activation. Instead you "build" an Alps
record and return it from either a Spring MVC or a Spring WebFlux web method as shown below:
Alps
record@GetMapping(value = "/profile", produces = ALPS_JSON_VALUE)
Alps profile() {
return Alps.alps() //
.doc(doc() //
.href("https://example.org/samples/full/doc.html") //
.value("value goes here") //
.format(Format.TEXT) //
.build()) //
.descriptor(getExposedProperties(Employee.class).stream() //
.map(property -> Descriptor.builder() //
.id("class field [" + property.getName() + "]") //
.name(property.getName()) //
.type(Type.SEMANTIC) //
.ext(Ext.builder() //
.id("ext [" + property.getName() + "]") //
.href("https://example.org/samples/ext/" + property.getName()) //
.value("value goes here") //
.build()) //
.rt("rt for [" + property.getName() + "]") //
.descriptor(Collections.singletonList(Descriptor.builder().id("embedded").build())) //
.build()) //
.collect(Collectors.toList()))
.build();
}
-
This example leverages
PropertyUtils.getExposedProperties()
to extract metadata about the domain object’s attributes.
This fragment has test data plugged in. It yields JSON like this:
{ "version": "1.0", "doc": { "format": "TEXT", "href": "https://example.org/samples/full/doc.html", "value": "value goes here" }, "descriptor": [ { "id": "class field [name]", "name": "name", "type": "SEMANTIC", "descriptor": [ { "id": "embedded" } ], "ext": { "id": "ext [name]", "href": "https://example.org/samples/ext/name", "value": "value goes here" }, "rt": "rt for [name]" }, { "id": "class field [role]", "name": "role", "type": "SEMANTIC", "descriptor": [ { "id": "embedded" } ], "ext": { "id": "ext [role]", "href": "https://example.org/samples/ext/role", "value": "value goes here" }, "rt": "rt for [role]" } ] }
Instead of linking each field "automatically" to a domain object’s fields, you can write them by hand if you like. It’s also possible
to use Spring Framework’s message bundles and the MessageSource
interface. This gives you the ability to delegate these values to
locale-specific message bundles and even internationalize the metadata.
4.7. Community-based media types
Thanks to the ability to create your own media type, there are several community-led efforts to build additional media types.
4.7.1. JSON:API
-
Media type designation:
application/vnd.api+json
-
Latest Release
-
Current Snapshot
-
Project Lead: Kai Toedter
<dependency>
<groupId>com.toedter</groupId>
<artifactId>spring-hateoas-jsonapi</artifactId>
<version>{see project page for current version}</version>
</dependency>
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'
Visit the project page for more details if you want snapshot releases.
4.7.2. Siren
-
Media type designation:
application/vnd.siren+json
-
Project Lead: Ingo Griebsch
<dependency>
<groupId>de.ingogriebsch.hateoas</groupId>
<artifactId>spring-hateoas-siren</artifactId>
<version>{see project page for current version}</version>
<scope>compile</scope>
</dependency>
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'
4.8. Registering a custom media type
Spring HATEOAS allows you to integrate custom media types through an SPI. The building blocks of such an implementation are:
-
Some form of Jackson
ObjectMapper
customization. In its most simple case that’s a JacksonModule
implementation. -
A
LinkDiscoverer
implementation so that the client-side support is able to detect links in representations. -
A small bit of infrastructure configuration that will allow Spring HATEOAS to find the custom implementation and pick it up.
4.8.1. Custom media type configuration
Custom media type implementations are picked up by Spring HATEOAS by scanning the application context for any implementations of the HypermediaMappingInformation
interface.
Each media type must implement this interface in order to:
-
Be applied to
WebClient
,WebTestClient
, orRestTemplate
instances. -
Support serving that media type from Spring Web MVC and Spring WebFlux controllers.
To define your own media type could look as simple as this:
@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {
@Override
public List<MediaType> getMediaTypes() {
return Collections.singletonList(MediaType.parseMediaType("application/vnd-acme-media-type")); (1)
}
@Override
public Module getJacksonModule() {
return new Jackson2MyMediaTypeModule(); (2)
}
@Bean
MyLinkDiscoverer myLinkDiscoverer() {
return new MyLinkDiscoverer(); (3)
}
}
1 | The configuration class returns the media type it supports. This applies to both server-side and client-side scenarios. |
2 | It overrides getJacksonModule() to provide custom serializers to create the media type specific representations. |
3 | It also declares a custom LinkDiscoverer implementation for further client-side support. |
The Jackson module usually declares Serializer
and Deserializer
implementations for the representation model types RepresentationModel
, EntityModel
, CollectionModel
and PagedModel
.
In case you need further customization of the Jackson ObjectMapper
(like a custom HandlerInstantiator
), you can alternatively override configureObjectMapper(…)
.
Prior versions of reference documentation has mentioned implementing the |
4.8.2. Recommendations
The preferred way to implement media type representations is by providing a type hierarchy that matches the expected format and can be serialized by Jackson as is.
In the Serializer
and Deserializer
implementations registered for RepresentationModel
, convert the instances into the media type-specific model types and then lookup the Jackson serializer for those.
The media types supported by default use the same configuration mechanism as third-party implementations would do.
So it’s worth studying the implementations in the mediatype
package.
Note, that the built in media type implementations keep their configuration classes package private, as they’re activated via @EnableHypermediaSupport
.
Custom implementations should probably make those public instead to make sure, users can import those configuration classes from their application packages.