A library implementing Siren as a custom Spring HATEOAS hypermedia type. Siren is a hypermedia specification for representing entities.

Copyright © 2019-2020 The original authors.

Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

1. Introduction

Siren: a hypermedia specification for representing entities
— Kevin Swiber
Siren specification

This library extends Spring HATEOAS with the custom hypermedia type Siren. The media type for Siren is defined as application/vnd.siren+json.

The current version of this library (version 1.4.0-SNAPSHOT) is based on Spring HATEOAS version 1.5.0-SNAPSHOT. The source of this library can be found here. The Javadoc API documentation can be found here.

For further understanding of this document, please be aware of both the Spring HATEOAS and the Siren documentation. The following documentation assumes that the reader knows above documents.

2. Setup

To enable the Siren hypermedia type you simply need to add this library as a dependency to your project. The library is accessible through Maven Central or one of its proxies.

If you use Apache Maven, add the following to your build file:

pom.xml
<dependency>
    <groupId>de.ingogriebsch.hateoas</groupId>
    <artifactId>spring-hateoas-siren</artifactId>
    <version>1.4.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

If you prefer to use Gradle, add the following to your build file:

build.gradle.kts
dependencies {
    implementation("de.ingogriebsch.hateoas:spring-hateoas-siren:1.4.0-SNAPSHOT")
}

Having this library on the classpath is all you need in a Spring Boot based project to get the hypermedia type automatically enabled.

If you want to use the library in a project that is not based on Spring Boot, you need to use class SirenMediaTypeConfiguration. This means, that you need to create an instance of this class and that you are responsible for that the methods that are necessary to initialize the library are executed.

This way incoming requests asking for the mentioned media type will get an appropriate response. The library is also able to deserialize a Json representation of the media type into corresponding representation models.

3. Server Side support

3.1. Serialization

Using this library will make your application respond to requests that have an Accept header of application/vnd.siren+json.

3.1.1. Representation Models

In general, each Spring HATEOAS representation model provided through a @RestController method is rendered into a Siren entity. Depending on the respective type of the representation model the following rules apply:

Representation Model

If this library serializes a representation model, it maps

Example 1. Serialize a representation model having some links

Define a representation model:

class PersonModel extends RepresentationModel<PersonModel> {
  String firstname, lastname;
}

Use the representation model:

PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
// add some links (having affordances) to the model...

The resulting Siren representation:

{
  "class": [
    ...
  ],
  "properties": {
    "firstname": "Dave",
    "lastname": "Matthews"
  },
  "links": [
    ...
  ],
  "actions": [
    ...
  ]
}
Entity Model

If this library renders an entity model, it maps

Example 2. Serialize an entity model wrapping a pojo and having some links

A person class:

class Person {
  String firstname, lastname;
}

An entity model wrapping a person object:

Person person = new Person();
person.firstname = "Dave";
person.lastname = "Matthews";

EntityModel<Person> model = EntityModel.of(person);
// add some links (having affordances) to the model...

The resulting Siren representation:

{
  "class": [
    ...
  ],
  "properties": {
    "firstname": "Dave",
    "lastname": "Matthews"
  },
  "links": [
    ...
  ],
  "actions": [
    ...
  ]
}
Example 3. Serialize an entity model wrapping an entity model wrapping a pojo and having some links

A person class:

class Person {
  String firstname, lastname;
}

An entity model wrapping a person object:

Person person = new Person();
person.firstname = "Dave";
person.lastname = "Matthews";

EntityModel<Person> personModel = EntityModel.of(person);
// add some links (having affordances) to the person model...

Another entity model wrapping the entity model:

EntityModel<EntityModel<Person>> model = EntityModel.of(personModel);
// add some links (having affordances) to the model...

The resulting Siren representation:

{
  "class": [
    ...
  ],
  "entities": [
    "class": [
      ...
    ],
    "rel": [
      ...
    ],
    "properties": {
      "firstname": "Dave",
      "lastname": "Matthews"
    }
  ],
  "links": [
    ...
  ],
  "actions": [
    ...
  ]
}
Collection Model

If this library renders a collection model, it maps

Example 4. Serialize a collection model wrapping some entity models and having some links

A person class:

class Person {
  String firstname, lastname;
}

Some entity models each wrapping a person object:

Person p1 = new Person();
p1.firstname = "Dave";
p1.lastname = "Matthews";

EntityModel<Person> pm1 = EntityModel.of(p1);
// add some links (having affordances) to the model...

Person p2 = new Person();
p2.firstname = "Stefan";
p2.lastname = "Lessard";

EntityModel<Person> pm2 = EntityModel.of(p2);
// add some links (having affordances) to the model...

A collection model wrapping the entity models:

Collection<EntityModel<Person>> people = Arrays.asList(pm1, pm2);
CollectionModel<EntityModel<Person>> people = CollectionModel.of(people);
// add some links (having affordances) to the model...

The resulting Siren representation:

{
  "class": [
    ...
  ],
  "entities": [{
    "class": [
      ...
    ],
    "properties": {
      "firstname": "Dave",
      "lastname": "Matthews"
    },
    "links": [
      ...
    ],
    "actions": [
      ...
    ]
  },{
    "class": [
      ...
    ],
    "properties": {
      "firstname": "Stefan",
      "lastname": "Lessard"
    },
    "links": [
      ...
    ],
    "actions": [
      ...
    ]
  }],
  "links": [
    ...
  ],
  "actions": [
    ...
  ]
}
Paged Model

If this library renders a paged model, it maps

Example 5. Serialize a paged model wrapping some entity models and having some links

A person class:

class Person {
  String firstname, lastname;
}

Some entity models each wrapping a person object:

Person p1 = new Person();
p1.firstname = "Dave";
p1.lastname = "Matthews";

EntityModel<Person> pm1 = EntityModel.of(p1);
// add some links (having affordances) to the model...

Person p2 = new Person();
p2.firstname = "Stefan";
p2.lastname = "Lessard";

EntityModel<Person> pm2 = EntityModel.of(p2);
// add some links (having affordances) to the model...

A paged model wrapping the entity models:

Collection<EntityModel<Person>> people = Collections.singleton(personModel);
PageMetadata metadata = new PageMetadata(20, 0, 1, 1);
PagedModel<EntityModel<Person>> model = PagedModel.of(people, metadata);
// add some links (having affordances) to the model...

The resulting Siren representation:

{
  "class": [
    ...
  ],
  "properties": {
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  },
  "entities": [{
    "class": [
      ...
    ],
    "properties": {
      "firstname": "Dave",
      "lastname": "Matthews"
    },
    "links": [
      ...
    ],
    "actions": [
      ...
    ]
  },{
    "class": [
      ...
    ],
    "properties": {
      "firstname": "Stefan",
      "lastname": "Lessard"
    },
    "links": [
      ...
    ],
    "actions": [
      ...
    ]
  }],
  "links": [
    ...
  ],
  "actions": [
    ...
  ]
}

If this library renders a link, it maps

If this library renders a link, it does not

  • map any links having an http method not equal to GET.

  • distinguish between templated and not templated links.

Example 6. Serialize a link having some affordances

A person class:

class Person {
  String firstname, lastname;
}

A person controller class:

@RestController
class PersonController {

  @GetMapping("/persons/{id}")
  ResponseEntity<EntityModel<Person>> findOne(Long id) { ... }

  @PutMapping("/persons/{id}")
  ResponseEntity<EntityModel<Person>> update(Long id, Person person) { ... }

  @DeleteMapping("/persons/{id}")
  ResponseEntity<Void> delete(Long id) { ... }
}

A self link having affordances created based on the available person controller methods:

@GetMapping("/persons/{id}")
ResponseEntity<EntityModel<Person>> findOne(Long id) {
  Person person = personService.findOne(id);

  Link selfLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel() //
    .andAffordance(afford(methodOn(controllerClass).update(id, null))) //
    .andAffordance(afford(methodOn(controllerClass).delete(id)));

  EntityModel<Person> model = EntityModel.of(person, selfLink);
  return ResponseEntity.ok(model);
}

The resulting Siren representation:

{
  ...
  "links": [{
    "rel": [
      "self"
    ],
    "href": "http://localhost:8080/persons/1"
  }],
  "actions": [{
    "name": "update",
    "method": "PUT",
    "href": "http://localhost:8080/persons/1",
    "fields": [{
      "name": "firstname",
      "type": "text"
    },{
      "name": "lastname",
      "type": "text"
    }]
  },{
    "name": "delete",
    "method": "DELETE",
    "href": "http://localhost:8080/persons/1"
  }]
}

3.1.3. Siren Model

Siren defines a resource as an entity which has not only properties and navigable links but may also contain embedded representations.

Because such representations retain all the characteristics of an entity you can build quite complex resource structures. Even if it is in most cases probably sufficient to simply use the available representation models it can be necessary in some cases to be able to build such quite complex structures.

Therefore this library provides a builder API that allows to build a Siren model which is then transfered into the respective Siren Entity structure. Means the library provides a SirenModelBuilder that allows to create RepresentationModel instances through a Siren idiomatic API.

3.2. Internationalization

Siren defines a title attribute for its entities, links and actions (including their fields). These titles can be populated by using Spring’s resource bundle abstraction together with a resource bundle named rest-messages. This bundle will be set up automatically and is used during the serialization process.

3.2.1. Entities

To define a title for a Siren entity, use the key template _entity.$type.title. The type used to build the resulting key depends on which type of Spring HATEOAS representation model is used. To evaluate if a title is available for a specific type, the fqcn will be checked first, followed by the simple name. Finally, it is checked whether type default is available.

To define a title for a Siren link, use the key template _link.$rel.title. To evaluate if a title is available for the link, the rel of the Spring HATEOAS link will be checked first. Finally, it is checked whether type default is available.

3.2.3. Actions

To define a title for a Siren action, use the key template _action.$name.title. To evaluate if a title is available for the action, the name of the Spring HATEOAS affordance will be checked first. Finally, it is checked whether type default is available.

To define a title for a Siren action field, use the key template _field.$name.title. To evaluate if a title is available for the action field, the name of the input property which is part of the Spring HATEOAS affordance will be checked first. Finally, it is checked whether type default is available.

3.3. Restrictions

Siren embedded links are currently not implemented through the library itself. If you want them, you need to implement a pojo representing an embedded link and add it as content of either a CollectionModel or PagedModel instance.

4. Client Side support

4.1. Deserialization

This library allows to use/handle the Siren hypermedia type on clients requesting data from servers producing this hypermedia type. This means that adding and enabling this library is sufficient to be able to deserialize responses containing data of the Siren hypermedia type into their respective representation models.

Please be aware of that the deserialization mechanism is currently not able to deserialize all types of complex Siren Entity structures that can be build with Siren model builder API.

Please be aware of that the deserialization mechanism is currently not able to deserialize a Siren action into the corresponding affordance model.

4.2. Traverson

The hypermedia type application/vnd.siren+json is currently not usable with the Traverson implementation provided through Spring HATEOAS.

When working with hypermedia enabled representations, a common task is to find a link with a particular relation type in it. Spring HATEOAS provides JsonPath-based implementations of the LinkDiscoverer interface for the configured hypermedia types. When using this library, an instance supporting this hypermedia type is exposed as a Spring bean.

Alternatively, you can set up and use an instance as follows:

String content = "{'_links' :  { 'foo' : { 'href' : '/foo/bar' }}}";

LinkDiscoverer discoverer = new SirenLinkDiscoverer();
Link link = discoverer.findLinkWithRel("foo", content);

assertThat(link.getRel(), is("foo"));
assertThat(link.getHref(), is("/foo/bar"));

5. Configuration

This library currently uses a really simple approach to map the respective representation model to the class attribute of the Siren entity. If you want to override/enhance this behavior you need to expose an implementation of the SirenEntityClassProvider interface as a Spring bean.

This library currently uses a really simple approach to evaluate the relation between a representation model and its contained representation model to set the rel attribute of the Siren entity. If you want to override/enhance this behavior you need to expose an implementation of the SirenEntityRelProvider interface as a Spring bean.

This library currently uses a really simple approach to map the respective type of a payload property of an affordance model to the type attribute of the Siren action field. If you need to specify additional mappings or if you want to override the default behavior, you can do so through the SirenConfiguration. If this is not enough you need to expose an implementation of the SirenActionFieldTypeConverter interface as a Spring bean. But then the support offered through the SirenConfiguration is not active anymore.

This library currently uses a really simple approach to instantiate the concrete instances of the representation models during the deserialization process. If you want to override/enhance this behavior you need to expose an implementation of the RepresentationModelFactories interface as a Spring bean.

6. Experimental

This section deals with experimental functions that are currently implemented but for which it is not yet clear whether they will find their way into the library.

6.1. Subclassing Specific Representation Models

Siren defines a resource as an entity which has not only properties and navigable links but may also contain embedded representations. Because such representations retain all the characteristics of an entity you can build quite complex resource structures.

It is in the nature of Spring HATEOAS' representation models of type RepresentationModel to be subclassed. But it is not intended to subclass the other representation model types, namely EntityModel, CollectionModel and PagedModel.

Even if this is not intended through Spring HATEOAS we 'bend' the intended behavior to allow to build such complex structures. This means that this library is able to handle subclassed representation models of type EntityModel and CollectionModel. It still makes no sense to subclass representation models of type PagedModel because they already contain specific properties explaining the nature of this type of resource.

To use this experimental feature you need to configure explicitly that you want to subclass the mentioned representation model types (i.e. this feature is disabled by default). You can enable this functionality in the following way:

@Configuration
public class HateoasConfiguration {

  @Bean
  public SirenConfiguration sirenConfiguration() {
    return new SirenConfiguration().withEntityAndCollectionModelSubclassingEnabled(true);
  }
}

The following example explains what is currently possible to do. We will skip parts of the Siren representation like class, links or actions and concentrate on the properties and embedded representations.

Example 7. Serialize subclassed collection model wrapping some entity models each wrapping a representation model

A representation model:

class Capital extends RepresentationModel<Capital> {
  String name;
}

An entity model:

class State extends EntityModel<Capital> {
  String name;
}

A collection model:

class Country extends CollectionModel<State> {
  String name;
}

Use the different types of representation models:

Capital denpasar = new Capital("Denpasar");
State bali = new State("Bali", denpasar);

Capital ambon = new Capital("Ambon");
State maluku = new State("Maluku", ambon);

Capital pekanbaru = new Capital("Pekanbaru");
State riau = new State("Riau", pekanbaru);

List<State> states = List.of(bali, maluka, riau);
Country indonesia = new Country("Indonesia", states);

The resulting Siren representation:

{
  "properties": {
    "name": "Indonesia"
  },
  "entities": [{
    "properties": {
      "name": "Bali"
    },
    "entities": [{
      "properties": {
        "name": "Denpasar"
      },
    }]
  }, {
    "properties": {
      "name": "Maluku"
    },
    "entities": [{
      "properties": {
        "name": "Ambon"
      },
    }]
  }, {
    "properties": {
      "name": "Riau"
    },
    "entities": [{
      "properties": {
        "name": "Pekanbaru"
      },
    }]
  }]
}

This is still a relatively simple example of what is possible if using subclassed representation models together. Especially mixing entity models with collection models and vice versa allows to build quite complex structures.

License

This code is open source software licensed under the Apache 2.0 License.