Share
Embed
JSON:API for Spring HATEOAS
This slide deck introduces my Open-Source Project "JSON:API for Spring HATEOAS", see https://github.com/toedter/spring-hateoas-jsonapi
Kai Toedter
March 18, 2022
Transcript
61 slides
-
Who am I? ▪ Distinguished Key Expert at Siemens ▪
Open-Source Lover & VR Gamer ▪ E-mail: [email protected] ▪ Mastodon: https://mastodon.social/@kaitoedter ▪ GitHub: github.com/toedter 6/6/2025 2 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. -
Show Hands! 6/6/2025 © Kai Tödter, Licensed under a Creative
Commons Attribution 4.0 International License. 3 -
HATEOAS ▪ Is for “Hypermedia As The Engine Of Application
State” ▪ Very hard to pronounce ☺ ▪ Key concept of REST ▪ WIKIPEDIA: With HATEOAS, a client interacts with a network application whose application servers provide information dynamically through hypermedia 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 4 -
JSON:API Web site: jsonapi.org “JSON:API is designed to minimize both
the number of requests and the amount of data transmitted between clients and servers. This efficiency is achieved without compromising readability, flexibility, or discoverability.” 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 5 -
Minimal JSON:API Example { "data": { "id": "1", "type": "movies",
"attributes": { "title": "The Shawshank Redemption", "year": 1994, "rating": 9.3, "rank": 1 } }, "links": { "self": "https://mymovies.com/api/movies/1" } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 6 -
Spring HATEOAS ▪ Spring basic framework for REST with Hypermedia
support ▪ Supports generic Hypermedia API ▪ Build-in Support for Representations like HAL, HAL-FORMS, UBER, Collection+JSON, … ▪ Community-based media types: JSON:API, Siren ▪ https://docs.spring.io/spring-hateoas/docs/current- SNAPSHOT/reference/html/ 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 7 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 8 Links -
Links ▪ Essential for hypermedia ▪ In REST: How to
navigate to a REST resource ▪ Link semantic/name is called link relation ▪ The relation between a REST resource and the target REST resource ▪ Links are well known from HTML, like <a href="url">link text</a> 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 9 -
Links in Spring HATEOAS Link link = new Link("/my-url"); ▪
A link automatically has a self relation Link link = new Link("/my-url", "my-rel"); ▪ A link with my-rel relation 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 10 -
Link Relations ▪ Many Link relations are standardized by IANA
▪ IANA = Internet Assigned Numbers Authority ▪ https://www.iana.org/assignments/link- relations/link-relations.xhtml ▪ Examples: self, item, next, last, … ▪ Recommendation: Before creating a custom name for a link relation, look up the IANA list! 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 11 -
Links are great! ▪ For providing navigation to useful other
REST resources ▪ For providing domain knowledge to the REST clients, so that they don’t have to compute domain state on there own 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 12 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 13 Representation Models -
Representation Models ▪ REST => Representational State Transfer ▪ Manipulation
of resources through their representations ▪ Domain Model != Representation Model ▪ Spring HATEOAS provides RepresentationModel abstraction 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 14 -
Spring HATEOAS RepresentationModel ▪ RepresentationModel ▪ Root class, for REST
item resources ▪ CollectionModel ▪ For REST collection resources ▪ EntityModel ▪ Convenient wrapper for converting a domain model into a representation model ▪ PagedModel ▪ Addition to CollectionModel for paged collections 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 15 -
Domain Model Example public class Director { private Long id;
private String name; public Director(String name) { this.name = name; } … } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 16 -
Controller Example @GetMapping("/directors/{id}") public ResponseEntity<EntityModel<Director>> findOne(@PathVariable Long id) { return
repository.findById(id) .map(director -> EntityModel.of(director) .add(linkTo(methodOn(DirectorController.class) .findOne(director.getId())).withSelfRel())) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 17 -
Response in HAL Media Type { "id": 2, "name": "Frank
Darabont", "_links": { "self": { "href": "http://localhost:8080/api/directors/2" } } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 18 -
JSON:API for Spring HATEOAS ▪ Open Source Project ▪ Apache
2 License ▪ https://github.com/toedter/spring-hateoas- jsonapi ▪ Reference Documentation ▪ API Documentation 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 20 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 21 -
Project Dependencies Maven: <dependency> <groupId>com.toedter</groupId> <artifactId>spring-hateoas-jsonapi</artifactId> <version>2.1.4</version> </dependency> Gradle: implementation
'com.toedter:spring-hateoas-jsonapi:2.1.4' 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 22 -
Domain Model Example public class Director { private Long id;
private String name; public Director(String name) { this.name = name; } … } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 23 -
Response in JSON:API media type { "data": { "id": "2",
"type": "directors", "attributes": { "name": "Frank Darabont" } }, "links": { "self": "http://localhost:8080/api/directors/2" } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 24 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 25 Annotations -
Annotations ▪ @JsonApiId to mark a JSON:API id ▪ @JsonApiType
to mark a JSON:API type ▪ @JsonApiTypeForClass to mark a class to provide a JSON:API type ▪ @JsonApiRelationships to mark a JSON:API relationship, only used for deserialization ▪ @JsonApiMeta to serialize/deserialize properties to JSON:API meta 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 26 -
Example with Annotations public class Movie { @Id private String
myId; @JsonApiType private String myType; @JsonApiMeta private String myMeta; private String title; } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 27 -
Annotations Example (2) EntityModel.of( new Movie("1", "MOVIE", "metaValue", "Star Wars"));
will be rendered as { "data": { "id": "1", "type": "MOVIE", "attributes": { "title": "Star Wars" }, "meta": { "myMeta": "metaValue" } } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 28 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 29 Builder -
JsonApiBuilder Movie movie = new Movie("1", "Star Wars"); final RepresentationModel<?>
jsonApiModel = jsonApiModel() .model(movie) .build(); 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 30 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 31 Relationships -
Relationships ▪ In JSON:API, relationships between REST resources are made
explicit, using the relationship object ▪ Relationships can be to-one or to-many ▪ Relationships must contain at least one of: ▪ links: a links object containing at least one of the following: ▪ self: a link for the relationship itself ▪ related: a related resource link ▪ data: resource linkage with id and type ▪ meta: meta object that contains non-standard meta- information about the relationship 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 32 -
Build Relationships Movie movie = new Movie("1", "Star Wars"); Director
director = new Director("1", "George Lucas"); final RepresentationModel<?> jsonApiModel = jsonApiModel() .model(movie) .relationship("directors", director) .build(); 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 33 -
Relationsship Example { "data": { "id": "1", "type": "movies", "attributes":
{ "title": "Star Wars" }, "relationships": { "directors": { "data": { "id": "1", "type": "directors" } } } } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 34 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 35 Inclusion -
Inclusion of Related Resources ▪ With included, you can include
the content of related recourses in the compound document ▪ The JsonApiBuilder supports adding ▪ A single included resource ▪ A collection of included resources ▪ The builder assures that included resources with same id and type appear only ONCE 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 36 -
Inclusion Example for (Movie movie : pagedResult.getContent()) { jsonApiModelBuilder.included(movie.getDirectors()); }
"included": [ { "id": "1", "type": "directors", "attributes": { "name": "Lana Wachowski" } }, … 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 37 -
Sparse Fieldsets Convenient way to specify which ▪ Attributes of
Resources ▪ Relationships (by name) ▪ Attributes of included Relationships will be included in the JSON response 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 38 -
Controller for Sparse Fieldset In a REST controller, a method
with HTTP- mapping could provide an optional request attribute for each sparse fieldset @GetMapping("/movies") public ResponseEntity<RepresentationModel<?>> findAll( @RequestParam(value = "included", required = false) String[] included, @RequestParam(value = "fields[movies]", required = false) String[] fieldsMovies) { 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 39 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 40 Sparse Fieldsets Demo -
Meta ▪ JSON:API Meta can be added using the builder
or by using the @JsonApiMeta annotation ▪ Paging information Meta can be added automatically => Use PagedModel 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 41 -
Pagination Example … "links": { "self": "http://localhost/movies", "first": "http://localhost/movies?page[number]=0&page[size]=2", "prev":
"http://localhost/movies?page[number]=0&page[size]=2", "next": "http://localhost/movies?page[number]=2&page[size]=2", "last": "http://localhost/movies?page[number]=49&page[size]=2" }, "meta": { "page": { "number": 1, "size": 2, "totalPages": 50, "totalElements": 100 } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 42 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 43 Configuration -
Configuration You can configure ▪ If the JSON:API version should
be rendered automatically, the default is false. ▪ If JSON:API types should be rendered as pluralized or non pluralized class names. ▪ The default is pluralized ▪ If JSON:API types should be rendered as lower cased or original class names. ▪ The default is lower cased ▪ If page information of a PagedModel should be rendered automatically as JSON:API meta object. ▪ The default is true ▪ If a specific Java class should be rendered with a specific JSON:API type. ▪ A lambda expression to add additional configuration to the Jackson ObjectMapper used for serialization. ▪ Experimental: Render Spring HATEOAS affordances as JSON:API link meta. 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 44 -
Configuration Example @Bean JsonApiConfiguration jsonApiConfiguration() { return new JsonApiConfiguration() .withJsonApiVersionRendered(true)
.withPluralizedTypeRendered(false) .withLowerCasedTypeRendered(false) .withTypeForClass(MyMovie.class, "my-movies") .withObjectMapperCustomizer( objectMapper -> objectMapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true)); } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 45 -
Error Handling To create JSON:API compliant error messages, you can
use JsonApiErrors and JsonApiError return ResponseEntity.badRequest().body( JsonApiErrors.create().withError( JsonApiError.create() .withAboutLink("http://movie-db.com/problem") .withTitle("Movie-based problem") .withStatus(HttpStatus.BAD_REQUEST.toString()) .withDetail("This is a test case"))); 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 46 -
Error Example { "errors": [ { "links": { "about": "http://movie-db.com/problem"
}, "status": "400 BAD_REQUEST", "title": "Movie-based problem", "detail": "This is a test case" } ] } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 47 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 48 Deserialization -
Deserialization ▪ We want to deserialize JSON input directly to
Spring HATEOAS representation models, e.g. for ▪ HTTP POST ▪ HTTP PATCH 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 49 -
Deserialization Example (1) { "data": { "type": "movies", "attributes": {
"title": "New Movie" }, "relationships": { "directors": { "data": [ { "id": "1", "type": "directors" }, { "id": "2", "type": "directors" } ] } } } } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 50 -
Deserialization Example (2) public class MovieWithDirectors extends Movie { @JsonApiType
String myType = "movies"; @JsonIgnore @JsonApiRelationships("directors") List<Director> directors; <= ONLY Director id will be set! } 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 51 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 52 Affordances -
“ The affordances of the environment are what it offers
… what it provides or furnishes, either for good or ill. The verb 'to afford' is found in the dictionary, but the noun 'affordance' is not. I have made it up.” James J. Gibson The Ecological Approach to Visual Perception (1986) 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 53 -
Affordances ▪ With Affordances, possible actions on a REST resource
(or Link relation) can be described in more detail, e.g. ▪ What can you do with a REST resource? ▪ Create, Update, Delete, … ▪ Which parameters are mandatory or optional ▪ How would (internationalized) labels be displayed in a user interface ▪ … 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 54 -
Spring HATEOAS Affordances Example © Kai Tödter, Licensed under a
Creative Commons Attribution 4.0 International License. 55 final Affordance newMovieAffordance = afford(methodOn(MovieController.class).newMovie(null)); Link selfLink = linkTo(MovieController.class).slash("movies" + uriParams + "page[number]=" + pagedResult.getNumber() + "&page[size]=" + pagedResult.getSize()) .withSelfRel() .andAffordance(newMovieAffordance); -
Rendered Affordance (1) 6/6/2025 © Kai Tödter, Licensed under a
Creative Commons Attribution 4.0 International License. 56 "links": { "self": { "href": "http://localhost:8080/api/movies?page[number]=0&page[size]=10", "meta": { "affordances": [ { "name": "newMovie", "link": { "rel": "newMovie", "href": "http://localhost:8080/api/movies" }, "httpMethod": "POST", ... -
Rendered Affordance (2) 6/6/2025 © Kai Tödter, Licensed under a
Creative Commons Attribution 4.0 International License. 57 "inputProperties": [ { "name": "imdbId", "type": "text" }, … { "name": "title", "type": "text", "required": true }, … ] -
Conclusion With JSON:API for Spring HATEOAS, it is very easy
to support JSON:API (serialization + deserialization) out of the box. With the builder, special JSON:API features like relationships and sparse fieldsets are supported as well. 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 58 -
6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution
4.0 International License. 59 Discussion -
Links ▪ Spring HATEOAS: https://github.com/spring-projects/spring-hateoas ▪ JSON:API for Spring HATEOAS:
https://github.com/toedter/spring-hateoas-jsonapi 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 60 -
License ▪ This work is licensed under a Creative Commons
Attribution 4.0 International License. ▪ See http://creativecommons.org/licenses/by/4.0/ 6/6/2025 © Kai Tödter, Licensed under a Creative Commons Attribution 4.0 International License. 61