This project provides JSON:API media type support for Spring HATEOAS.

© 2024 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.

Project Metadata

1. Introduction

1.1. Fundamentals

JSON:API for Spring HATEOAS is based on version 2.3.1 of Spring HATEOAS. For further understanding of this document, please be aware of both

The following documentation assumes that the reader knows the above documents. Some parts of the Java code examples are folded, click on the icon in the bottom-right corner to expand the unfolded source code.

1.2. JSON:API

JSON:API is a widely adapted hypermedia format. You can find a list of implementations and tools here. Answers to a few questions, e.g. related to HAL, you can find here.

Anytime a client supplies an Accept header with application/vnd.api+json, you can expect something like this:

{
  "jsonapi": {
    "version": "1.1"
  },
  "data": [
    {
      "id": "1",
      "type": "movies",
      "attributes": {
        "title": "The Shawshank Redemption",
        "year": 1994,
        "rating": 9.3
      },
      "relationships": {
        "directors": {
          "data": [
            {
              "id": "2",
              "type": "directors"
            }
          ],
          "links": {
            "self": "http://localhost:8080/api/movies/1/relationships/directors",
            "related": "http://localhost:8080/api/movies/1/directors"
          }
        }
      },
      "links": {
        "self": "http://localhost:8080/api/movies/1"
      }
    }
  ],
  "included": [
    {
      "id": "2",
      "type": "directors",
      "attributes": {
        "name": "Frank Darabont"
      }
    }
  ],
  "links": {
    "self": "http://localhost:8080/api/movies?page%5Bnumber%5D=0&page%5Bsize%5D=1",
    "next": "http://localhost:8080/api/movies?page%5Bnumber%5D=1&page%5Bsize%5D=1",
    "last": "http://localhost:8080/api/movies?page%5Bnumber%5D=249&page%5Bsize%5D=1"
  },
  "meta": {
    "page": {
      "size": 1,
      "totalElements": 250,
      "totalPages": 250,
      "number": 0
    }
  }
}
The characters [ and ] in the Links section are unsafe and URL encoded when added automatically by the library. So the URL decoded next link would look like: http://localhost:8080/api/movies?page[number]=1&page[size]=1.

2. Setup

To enable the JSON:API media type you just need to add this module as a dependency to your project.

Gradle
implementation 'com.toedter:spring-hateoas-jsonapi:2.1.0'
Maven
<dependency>
    <groupId>com.toedter</groupId>
    <artifactId>spring-hateoas-jsonapi</artifactId>
    <version>2.1.0</version>
</dependency>

The latest published snapshot version is 2.1.1-SNAPSHOT. The 2.x.x stream is based on Spring Boot 3.x.x.

3. Server Side support

3.1. Representation Models

All Spring HATEOAS representation models are rendered as JSON:API. Consider a simple Movie class as the base for a Spring HATEOAS entity model:

@Data
@NoArgsConstructor
@AllArgsConstructor
@With

public class Movie {
    private String id;
    private String title;
}

An EntityModel.of(new Movie("1", "Star Wars")) is then rendered as

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  }
}

In JSON:API, the id field must be of type String. But in your model you can use any Class and toString() is used for conversion. So, if the id attribute of Movie would be of type long, the rendered JSON:API would be the same. The JSON:API type is automatically generated of the pluralized, lower case, simple class name. This is best practice, since then most likely the type matches the URL (end) of the corresponding REST collection resource.

You can configure if you want to use non-pluralized class names, see Configuration

If Spring HATEOAS links only contain a href, the simple JSON:API link is used to render JSON:API links. Here is an example of a simple self link:

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  },
  "links": {
    "self": "http://localhost/movies/1"
  }
}

A complex Link object of Spring HATEOAS can have optional properties like name, type, hreflang, title and others. In previous versions of this library, those properties were serialized as JSON:API link meta, e.g.

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  },
  "links": {
    "self": {
      "href": "https://complex-links.org",
      "meta": {
        "hreflang": "EN",
        "media": "media",
        "title": "title",
        "type": "type",
        "name": "name"
      }
    }
  }
}

JSON:API 1.1 now defines the optional link properties type, title and hreflang, see JSON:API Link Objects. The default rendering of a complex link is now

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  },
  "links": {
    "self": {
      "href": "https://complex-links.org",
      "title": "title",
      "type": "type",
      "hreflang": "EN",
      "meta": {
        "media": "media",
        "name": "name"
      }
    }
  }
}

As you see, the properties title, type, and hreflang appear now only as top-level link properties. So the new format reflects the new JSON:API 1.1 link structure, but is not backward compatible to version 1.x.x of this library.

If you want to stay backward-compatible and render the link properties type, title and hreflang both as top-level link properties and in the meta section, you can configure this behaviour, (see [links-configuration]). The rendered result would then look like:

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  },
  "links": {
    "self": {
      "href": "https://complex-links.org",
      "title": "title",
      "type": "type",
      "hreflang": "EN",
      "meta": {
        "hreflang": "EN",
        "media": "media",
        "title": "title",
        "type": "type",
        "name": "name"
      }
    }
  }
}

JSON:API is very strict about the allowed link relations, the allowed top-level links are self, related, describedBy, next, prev, first and last. The only allowed resource link is self.

By default, all other links will not be serialized. If you want to serialize links that are non-compliant with JSON:API, you can use a specific configuration, (see [links-configuration]).

3.3. Annotations

The goal of this implementation is to automate the mapping from/to JSON:API as convenient as possible.

There are four new annotations provided by this project:

  • @JsonApiId to mark a JSON:API id

  • @JsonApiType to mark a field or method to provide a JSON:API type

  • @JsonApiTypeForClass to mark class to provide a JSON:API type

    • The JSON:API type is a required value of this annotation

  • @JsonApiRelationships to mark a JSON:API relationship

  • @JsonApiMeta to mark a field or method to provide a JSON:API meta information

    • This annotation works for serialization and deserialization.

The use of these annotations is optional. For the mapping of the id, the following rules apply in order:

  • the annotation @JsonApiId is used on a field

  • the annotation @JsonApiId is used on a method

  • the annotation @Id (jakarta.persistence.Id) is used on a field

  • the annotation @Id (jakarta.persistence.Id) is used on a method

  • the entity (base for representation models) provides an attribute id

For the mapping of the type, the following rules apply in order:

  • the annotation @JsonApiTypeForClass is used on a class

  • the annotation @JsonApiType is used on a field

  • the annotation @JsonApiType is used on a method

  • if no annotation is present, the pluralized, lower case, simple class name of the entity will be used

You can configure if you want to use non-pluralized class names, see Configuration

As an example, consider the class

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor

public class MovieWithAnnotations {
    @Id
    private String myId;
    @JsonApiType
    private String type;

    @JsonApiMeta
    private String myMeta;

    private String title;
}

Then, EntityModel.of(new MovieWithAnnotations("1", "my-movies", "metaValue", "Star Wars"))) will be rendered as

{
  "data": {
    "id": "1",
    "type": "my-movies",
    "attributes": {
      "title": "Star Wars"
    },
    "meta": {
      "myMeta": "metaValue"
    }
  }
}

3.4. JSON:API Builder

If you want to use JSON:API relationships or included data, you can use the JsonApiModelBuilder. The following example shows how to create a JSON::API representation model using the JsonApiModelBuilder

import static com.toedter.spring.hateoas.jsonapi.JsonApiModelBuilder.jsonApiModel;
Movie movie = new Movie("1", "Star Wars");
final RepresentationModel<?> jsonApiModel = jsonApiModel().model(movie).build();

Consider that you want to express the relationships of movies to their directors. A simple Director class could look like:

@Data
@NoArgsConstructor
@AllArgsConstructor
@With

public class Director {
    private String id;
    private String name;
}

You can build a relationship from a movie to a director like

Movie movie = new Movie("1", "Star Wars");
Director director = new Director("1", "George Lucas");

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(movie)
                .relationship("directors", director)
                .build();

The representation model will be rendered as

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    },
    "relationships": {
      "directors": {
        "data": {
          "id": "1",
          "type": "directors"
        }
      }
    }
  }
}

If you want the directors relationship always being rendered as an array, even if it is empty or contains only a single data element, you can build it like:

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(EntityModel.of(movie))
                .relationshipWithDataArray("directors")
                .relationship("directors", director)
                .build();

The representation model will be rendered as

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    },
    "relationships": {
      "directors": {
        "data": [
          {
            "id": "3",
            "type": "directors"
          }
        ]
      }
    }
  }
}

You can also pass a Java Collection as data for a relationship. A collection will always be rendered as JSON array, even when it is empty or contains a single element. So,

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(EntityModel.of(movie))
                .relationship("directors", Collections.singletonList(director))
                .build();

would be rendered exactly like the previous example.

The builder also provides methods for adding links and meta to a relationship. Check out the Javadoc API documentation for more details.

If you want to include the related resources in the JSON:API output, you can build included director resources like:

Movie movie = new Movie("1", "The Matrix");
Movie relatedMovie = new Movie("2", "The Matrix 2");
Director director1 = new Director("1", "Lana Wachowski");
Director director2 = new Director("2", "Lilly Wachowski");

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(movie)
                .relationship("directors", director1)
                .relationship("directors", director2)
                .relationship("relatedMovies", relatedMovie)
                .included(director1)
                .included(director2)
                .build();

The representation model will be rendered as

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "The Matrix"
    },
    "relationships": {
      "relatedMovies": {
        "data": {
          "id": "2",
          "type": "movies"
        }
      },
      "directors": {
        "data": [
          {
            "id": "1",
            "type": "directors"
          },
          {
            "id": "2",
            "type": "directors"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "directors",
      "attributes": {
        "name": "Lana Wachowski"
      }
    },
    {
      "id": "2",
      "type": "directors",
      "attributes": {
        "name": "Lilly Wachowski"
      }
    }
  ]
}

The following example shows the creation of a more complex JSON:API specific representation model with a paged model as the base. The builder supports adding both pagination metadata and pagination links.

Movie movie = new Movie("1", "The Matrix");
Movie relatedMovie = new Movie("2", "The Matrix 2");
Director director1 = new Director("1", "Lana Wachowski");
Director director2 = new Director("2", "Lilly Wachowski");

final RepresentationModel<?> jsonApiModel1 =
        jsonApiModel()
                .model(movie)
                .relationship("directors", director1)
                .relationship("directors", director2)
                .relationship("relatedMovies", EntityModel.of(relatedMovie))
                .build();

Movie movie2 = new Movie("3", "Star Wars");
Director director3 = new Director("3", "George Lucas");

final RepresentationModel<?> jsonApiModel2 =
        jsonApiModel()
                .model(movie2)
                .relationship("directors", director3)
                .build();

List<RepresentationModel<?>> movies = new ArrayList<>();
movies.add(jsonApiModel1);
movies.add(jsonApiModel2);

PagedModel.PageMetadata pageMetadata = new PagedModel.PageMetadata(10, 1, 100, 10);
Link selfLink = Link.of("http://localhost/movies").withSelfRel();
final PagedModel<RepresentationModel<?>> pagedModel = PagedModel.of(movies, pageMetadata, selfLink);

RepresentationModel<?> pagedJasonApiModel =
        jsonApiModel()
                .model(pagedModel)
                .included(director1)
                .included(director2)
                .included(director3)
                .pageMeta()
                .pageLinks("http://localhost/movies")
                .build();

This model will be rendered as

{
  "data": [
    {
      "id": "1",
      "type": "movies",
      "attributes": {
        "title": "The Matrix"
      },
      "relationships": {
        "relatedMovies": {
          "data": {
            "id": "2",
            "type": "movies"
          }
        },
        "directors": {
          "data": [
            {
              "id": "1",
              "type": "directors"
            },
            {
              "id": "2",
              "type": "directors"
            }
          ]
        }
      }
    },
    {
      "id": "3",
      "type": "movies",
      "attributes": {
        "title": "Star Wars"
      },
      "relationships": {
        "directors": {
          "data": {
            "id": "3",
            "type": "directors"
          }
        }
      }
    }
  ],
  "included": [
    {
      "id": "1",
      "type": "directors",
      "attributes": {
        "name": "Lana Wachowski"
      }
    },
    {
      "id": "2",
      "type": "directors",
      "attributes": {
        "name": "Lilly Wachowski"
      }
    },
    {
      "id": "3",
      "type": "directors",
      "attributes": {
        "name": "George Lucas"
      }
    }
  ],
  "links": {
    "self": "http://localhost/movies",
    "first": "http://localhost/movies?page%5Bnumber%5D=0&page%5Bsize%5D=10",
    "prev": "http://localhost/movies?page%5Bnumber%5D=0&page%5Bsize%5D=10",
    "next": "http://localhost/movies?page%5Bnumber%5D=2&page%5Bsize%5D=10",
    "last": "http://localhost/movies?page%5Bnumber%5D=9&page%5Bsize%5D=10"
  },
  "meta": {
    "page": {
      "size": 10,
      "totalElements": 100,
      "totalPages": 10,
      "number": 1
    }
  }
}

The JSON:API specification allows empty to-one relationships and empty to-many relationships (see JSON:API specification). An empty to-one relationship can be added like

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(movie)
                .relationship("director", (Object)null)
                .build();

An empty to-many relationship can be added like

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(movie)
                .relationship("directors", Collections.emptyList())
                .build();

3.5. Inclusion of related Resources

There is no direct support for automatically including related resources, but a REST controller could provide an optional request parameter like

@RequestParam(value = "include", required = false) String[] include

Then, within the controller implementation, this parameter could be interpreted, and the builder could be used for the inclusion, like

if (include != null && include.length == 1 && include[0].equals(DIRECTORS)) {
    for (Movie movie : pagedResult.getContent()) {
        jsonApiModelBuilder.included(movie.getDirectors());
    }
}

Duplicated included directors will be eliminated automatically.

3.6. Nesting of JsonApiModels

When using the model builder, JsonApiModel instances can be used as the model and included resources. Here is an example that also illustrates the different levels of meta.

Director director = new Director("3", "George Lucas");
final RepresentationModel<?> directorModel =
        jsonApiModel()
                .model(EntityModel.of(director))
                .meta("director-meta", "director-meta-value")
                .build();

Map<String, Object> relationshipMeta = new HashMap<>();
relationshipMeta.put("relationship-meta", "relationship-meta-value");

Map<String, Object> directorRelationshipMeta = new HashMap<>();
directorRelationshipMeta.put("director-relationship-meta", "director-relationship-meta-value");

Movie movie = new Movie("1", "Star Wars");
final RepresentationModel<?> movieModel =
        jsonApiModel()
                .model(movie)
                .meta("movie-meta", "movie-meta-value")
                .relationship("directors", director, directorRelationshipMeta)
                .relationship("directors", relationshipMeta)
                .build();

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(movieModel)
                .meta("top-level-meta", "top-level-meta-value")
                .included(directorModel)
                .build();

This model will be rendered as

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    },
    "relationships": {
      "directors": {
        "data": {
          "id": "3",
          "type": "directors",
          "meta": {
            "director-relationship-meta": "director-relationship-meta-value"
          }
        },
        "meta": {
          "relationship-meta": "relationship-meta-value"
        }
      }
    },
    "meta": {
      "movie-meta": "movie-meta-value"
    }
  },
  "included": [
    {
      "id": "3",
      "type": "directors",
      "attributes": {
        "name": "George Lucas"
      },
      "meta": {
        "director-meta": "director-meta-value"
      }
    }
  ],
  "meta": {
    "top-level-meta": "top-level-meta-value"
  }
}

3.7. Sparse Fieldsets

Sparse fieldsets are supported for attributes within data and included. You can add sparse fieldsets by using the JsonApiBuilder. The following example illustrates the build, assuming a director would have the attributes name and born, and a movie would have the attributes title and rating.

MovieWithRating movie = new MovieWithRating("1", "Star Wars", 8.6);
DirectorWithMovies director = new DirectorWithMovies("3", "George Lucas", 1944);
director.setMovies(Collections.singletonList(movie));

final RepresentationModel<?> jsonApiModel =
        jsonApiModel()
                .model(EntityModel.of(director))
                .fields("directors", "name")
                .fields("movies", "title")
                .relationship("movies", movie)
                .included(movie)
                .build();

So, only the name attribute of a director, and the title attribute of a movie would be serialized:

{
  "data": {
    "id": "3",
    "type": "directors",
    "attributes": {
      "name": "George Lucas"
    },
    "relationships": {
      "movies": {
        "data": {
          "id": "1",
          "type": "movies"
        }
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "movies",
      "attributes": {
        "title": "Star Wars"
      }
    }
  ]
}

In a REST controller, a method with HTTP-mapping could provide an optional request attribute for each sparse fieldset that should be applied to a specific JSON:API type, like:

@RequestParam(value = "fields[movies]", required = false) String[] fieldsMovies,
@RequestParam(value = "fields[directors]", required = false) String[] fieldsDirectors)

In the following controller code, you could check the existence of these request parameters, like:

if (fieldsDirectors != null) {
    builder = builder.fields("directors", fieldsDirectors);
}

When adding sparse fieldsets to the builder, they will NOT automatically exclude added relationships. Relationships have to be added conditionally, like the inclusions, for example:

if (fieldsDirectors = null || Arrays.asList(fieldsDirectors).contains("movies")) {
    builder = builder.relationship("movies", director.getMovies());
}

3.8. Spring HATEOAS Affordances

Spring HATEOAS provides a generic, media type independent API for 2.3.1/reference/html/#server.affordances[affordances]. Within the JSON:API spec, there is nothing equivalent, but JSON:API allows that links have additional meta information. This library provide a new experimental configuration to render Spring HATEOAS affordances as JSON:API link meta. Currently, a proprietary format is supported as well as the HAL-FORMS template format, that can be serialized by Spring HATEOAS out of the box.

The following example shows the usage of Spring HATEOAS affordances. First you have to enable this experimental feature in the configuration, like

new JsonApiConfiguration().withAffordancesRenderedAsLinkMeta(
   JsonApiConfiguration.AffordanceType.SPRING_HATEOAS);

Then you could add an affordance (for creating a movie) to a Spring HATEOAS link like

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);

The rendered result of the self link would then be

"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",
          "inputProperties": [
            {
              "name": "imdbId",
              "type": "text",
            },
            {
              "name": "rank",
              "type": "number",
            },
            {
              "name": "rating",
              "type": "number",
            },
            {
              "name": "title",
              "type": "text",
              "required": true
            },
            {
              "name": "year",
              "type": "number",
            }
          ]
        }
      ]
    }
  }
}
This feature is experimental. The JSON structure of the provided affordances might have breaking changes in upcoming releases.
If you want to get property information like required, you have to include a dependency to javax.validation:validation-api and annotate the required fields with @NotNull

3.9. Creating Resources with HTTP POST

To create new REST resources using HTTP POST, you can provide JSON:API formatted JSON as input. For example, a POST with the body:

{
  "data": {
    "type": "movies",
    "attributes": {
      "title": "Batman Begins"
    }
  }
}

will be deserialized to an EntityModel<Movie> automatically. You can also create REST resources that contain JSON:API relationships. You just have to annotate the underlying domain model class, with JsonApiRelationships(<relationship name>)

For example, a POST with the body:

{
  "data": {
    "type": "movies",
    "attributes": {
      "title": "New Movie"
    },
    "relationships": {
      "directors": {
        "data": [
          {
            "id": "1",
            "type": "directors"
          },
          {
            "id": "2",
            "type": "directors"
          }
        ]
      }
    }
  }
}

will be deserialized to an EntityModel<Movie> with a filled list of directors, where ONLY the id attribute of each director is set. The REST controller then has to interpret those relationships and bind the real director objects to the movie.

Here is an example of a class using the annotation:

@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@With

public class MovieWithDirectors extends Movie {
    @JsonApiType
    String myType = "movies";

    @JsonIgnore
    @JsonApiRelationships("directors")
    List<Director> directors;
}

If you want to set the attributes of a related director, you could put the director resource in the included section of the JSON, like

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    },
    "relationships": {
      "directors": {
        "data": {
          "id": "3",
          "type": "directors"
        }
      }
    }
  },
  "included": [
    {
      "id": "3",
      "type": "directors",
      "attributes": {
        "name": "George Lucas"
      }
    }
  ]
}

In this case, after deserialization the name attribute of the first director is set to "George Lucas".

If you use the annotation JsonApiRelationships on an attribute of a Java class, the content will NOT be serialized automatically to JSON:API relationships. This is on purpose, please use the JsonApiModelBuilder to decide, which relationships and included objects you want to return.

3.10. Deserialization of JSON:API types

If entities contain an explicit @JsonApiType field annotation, those fields are also filled during deserialization. This is also true for relationships if the relationship entity contains an explicit @JsonApiType annotation.

Consider the following classes:

@Data
@NoArgsConstructor
@AllArgsConstructor
@With

public class DirectorWithType {
    private String id;
    private String name;

    @JsonApiType
    private  String directorType;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@With

public class MovieWithTypedDirectors {
    private String id;
    private String title;

    @JsonApiType
    String myType;

    @JsonIgnore
    @JsonApiRelationships("directors")
    List<DirectorWithType> directors;
}

Then the following JSON

{
  "data": {
    "type": "movies",
    "attributes": {
      "title": "New Movie"
    },
    "relationships": {
      "directors": {
        "data": [
          {
            "id": "1",
            "type": "director-type-1"
          },
          {
            "id": "2",
            "type": "director-type-2"
          }
        ]
      }
    }
  }
}

will be deserialized to a MovieWithTypedDirectors where myType is "movies" and 2 (empty) DirectorWithType objects. The first DirectorWithType object with id = '1' and directorType = "director-type-1", the second DirectorWithType object with id = '2' and directorType = "director-type-2",

Currently, only List and Set are supported collection classes.

3.11. Deserialization of PagedModels

While a server implementation of HTTP POST and PATCH takes single resources as input, it is sometimes handy to be able to deserialize collection models and paged models. This is useful if a service consumes results of other services that produce JSON:API responses.

Here is an example of a serialized PagedModel.

{
  "data": [
    {
      "id": "1",
      "type": "movies",
      "attributes": {
        "title": "Star Wars"
      },
      "links": {
        "imdb": "https://www.imdb.com/title/tt0076759/?ref_=ttls_li_tt"
      }
    },
    {
      "id": "2",
      "type": "movies",
      "attributes": {
        "title": "Avengers"
      },
      "links": {
        "imdb": "https://www.imdb.com/title/tt0848228/?ref_=fn_al_tt_1"
      }
    }
  ],
  "links": {
    "next": "http://localhost/movies?page[number]=2&page[size]=2"
  },
  "meta": {
    "page": {
      "size": 2,
      "totalElements": 2,
      "totalPages": 2,
      "number": 1
    }
  }
}

If you deserialize the above JSON to a PagedModel<EntityModel<Movie>>>: The page meta information will be deserialized, as well as the links in both movie entity models. The same mechanism would work also for CollectionModel<EntityModel<Movie>>>.

3.12. UUID Deserialization

UUIDs (java.util.UUID) are supported natively for deserialization. So a JSON like

{
  "data": {
    "id": "00000000-0001-e240-0000-00002f08ba38",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  }
}

would be correctly deserialized to an object of class

@Data
@NoArgsConstructor
@AllArgsConstructor
@With
@JsonApiTypeForClass("movies")

public class MovieWithUUID {
    private UUID id;
    private String title;
}

3.13. Polymorphic Deserialization

The easiest way for polymorphic deserialization is to use the JsonApiConfiguration for assigning a JSON:API type to a Java class and then also switching on, that the mappings should also be used for deserialization, e.g.

@Bean
JsonApiConfiguration jsonApiConfiguration() {
    return new JsonApiConfiguration()
            .withTypeForClass(MovieSubclass.class, "my-movies")
            .withTypeForClassUsedForDeserialization(true));
}

Then a POST to a controller method like

@PostMapping("/movies")
public ResponseEntity<?> newMovie(@RequestBody EntityModel<Movie> movie) { ...

with JSON like

{
  "data": {
    "type": "my-movies",
    "attributes": {
      "title": "Batman Begins"
    }
  }
}

would be deserialized to a Java class of type MovieSubclass. Be aware that this mechanism overrides the default deserialization to an object of the class given by the REST controller method’s signature. The deserializer checks if the mapped Java type is assignable to the originally required Java type, otherwise an IllegalArgumentException is thrown.

The same mechanism can also be used to deserialize polymorphic relationships.

Consider the following class:

@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@With

public class MovieWithDirectors extends Movie {
    @JsonApiType
    String myType = "movies";

    @JsonIgnore
    @JsonApiRelationships("directors")
    List<Director> directors;
}

and a JsonApiConfiguration that looks like

@Bean
JsonApiConfiguration jsonApiConfiguration() {
    return new JsonApiConfiguration()
        .withTypeForClass(DirectorWithEmail.class,  "directors-with-email")
        .withTypeForClassUsedForDeserialization(true));
}

Then an HTTP POST to /movies with body

{
  "data": {
    "type": "movies",
    "attributes": {
      "title": "New Movie"
    },
    "relationships": {
      "directors": {
        "data": [
          {
            "id": "1",
            "type": "directors"
          },
          {
            "id": "2",
            "type": "directors-with-email"
          }
        ]
      }
    }
  }
}

would create 2 directors in the directors list, both empty except for the id field. But the first director would be an instance of class Director, while the second director would be an instance of class DirectorWithEmail.

3.13.1. Jackson Annotations

If the above mechanism does not fit your needs, you can also configure polymorphic deserialization on a 'per class' base, using some Jackson annotations. The following example illustrates this:

Imagine a controller method like

@PostMapping("/movies")
public ResponseEntity<?> newMovie(@RequestBody EntityModel<Movie> movie) { ...

and a subclass of Movie that contains a rating, like

@NoArgsConstructor
@Data
@JsonApiTypeForClass("movies")
public class MovieWithRating extends Movie {
    private double rating;
}

You could now annotate the Movie class with:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
    @JsonSubTypes.Type(value = MovieWithRating.class, name = "movieWithRating")
})

Then an HTTP POST to /movies with body

{
  "data": {
    "type": "movies",
    "attributes": {
      "@type": "movieWithRating",
      "title": "Batman Begins",
      "rating": 8.2
    }
  }
}

would be deserialized to an object of class MovieWithRating, even though the controller method accepts the superclass Movie.

@-Members were introduced in JSON:API version 1.1, see jsonapi.org/format/#document-member-names-at-members. From the JSON:API 1.1 spec: …​an @-Member that occurs in an attributes object is not an attribute.

Important: The above mechanism is also used for serialization, so you could set the JSON:API type attribute (within data) to a more generic type, but still serialize the @type attribute to indicate the specialized type. So the JSON of the serialized Java object (of class MovieWithRating) would then look like:

{
  "data": {
    "id": "3",
    "type": "movies",
    "attributes": {
      "@type": "movieWithRating",
      "title": "Batman Begins",
      "rating": 8.2
    }
  },
  "links": {
    "self": "http://localhost/movies/3"
  }
}

It is also possible to use Jackson’s @JsonSubTypes annotation for polymorphic relationships. Here is an example:

@NoArgsConstructor
public class PolymorphicRelationEntity {
    @JsonApiId
    private String id;

    @JsonApiType
    private final String type = null;

    @JsonApiRelationships("superEntities")
    @JsonIgnore
    @Getter
    private final List<SuperEntity<?>> relation = null;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = SuperEChild.class, name = "superEChild"),
        @JsonSubTypes.Type(value = SuperEChild2.class, name = "superEChild2")
})
public interface SuperEntity<T> {
    T getT();
}
@NoArgsConstructor
public class SuperEChild<T extends Collection<?>> implements SuperEntity<T> {
    @JsonApiId
    private String id;

    @JsonApiType
    private final String type = null;

    @Override
    public T getT() {
        return null;
    }
}
@NoArgsConstructor
public class SuperEChild2 implements SuperEntity<String> {
    @JsonApiId
    private String id;

    @JsonApiType
    private final String type = null;

    private final String extraAttribute = "";

    @Override
    public String getT() {
        return null;
    }
}

Then a JSON like

{
  "data": {
    "id": "poly123",
    "type": "polymorphicRelationEntity",
    "relationships": {
      "superEntities": {
        "data": [
          {
            "id": "456",
            "type": "superEChild"
          },
          {
            "id": "789",
            "type": "superEChild2"
          }
        ]
      }
    }
  }
}

would be deserialized to a PolymorphicRelationEntity with 2 relationships, the first one of Class SuperEChild, the second one of class SuperEChild2.

Right now there is the restriction, that the type attribute of both SuperEChild and SuperEChild2 has to be type. E.g. _type would not work.

3.14. Error Handling

To create JSON:API compliant error messages, you can use JsonApiErrors and JsonApiError

Here is an example of how to produce an error response:

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")));

The result would be rendered as:

{
  "errors": [
    {
      "links": {
        "about": "http://movie-db.com/problem"
      },
      "status": "400 BAD_REQUEST",
      "title": "Movie-based problem",
      "detail": "This is a test case"
    }
  ]
}

3.14.1. More Generic Error Handling

If you want to implement a more generic error handling that converts exceptions to JSON:API error responses, you could implement a @ControllerAdvice like:

@ControllerAdvice(annotations = RestController.class)
@Slf4j
public class ExceptionControllerAdvice {
    /**
     * Handles all manually thrown exceptions of type JsonApiErrorsException.
     */
    @ExceptionHandler
    public ResponseEntity<JsonApiErrors> handle(JsonApiErrorsException ex) {
        log.error("JSON:API error: Http status:{}, message:{}", ex.getStatus(), ex.getMessage());
        return ResponseEntity.status(ex.getStatus()).body(ex.getErrors());
    }

    /**
     * Handles all exceptions which are not of type JsonAPIErrorsException and treats
     * them as internal errors or maps specific exceptions to specific HTTP status codes.
     */
    @ExceptionHandler
    public ResponseEntity<JsonApiErrors> handle(Exception ex) {
        log.error("Internal error: {}", ex.getMessage());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).
                body(JsonApiErrors.create().withError(
                        CommonErrors.newInternalServerError().withDetail(ex.getMessage())));
    }
}

This would then convert exceptions to JSON:API error responses like:

{
    "errors": [
        {
            "id": "58bab604-a149-452a-ab30-f61fafab80e7",
            "status": "500",
            "code": "xrn:err:platform:internalServerError",
            "title": "Internal server error",
            "detail": "JSON parse error: Illegal unquoted character ((CTRL-CHAR, code 13)): has to be escaped using backslash to be included in string value"
        }
    ]
}

Furthermore, you could implement a JsonApiErrorsException like:

@Getter
public class JsonApiErrorsException extends RuntimeException {
    private final transient JsonApiErrors errors;
    private final HttpStatus status;

    public JsonApiErrorsException(JsonApiErrors errors, HttpStatus status) {
        super();
        this.errors = errors;
        this.status = status;
    }

    public JsonApiErrorsException(JsonApiError error) {
        super();
        this.errors = JsonApiErrors.create().withError(error);
        this.status = HttpStatus.valueOf(Integer.parseInt(error.getStatus()));
    }
}

and provide CommonErrors like:

public class CommonErrors {
    private CommonErrors() {
    }

    private static final JsonApiError resourceNotFound = JsonApiError.create()
        .withCode("xrn:err:platform:resourceNotFound")
        .withTitle("Resource Not Found")
        .withStatus("404");

    private static final JsonApiError badRequest = JsonApiError.create()
            .withCode("xrn:err:platform:badRequest")
            .withTitle("Bad Request")
            .withStatus("400");

    private static final JsonApiError internalServerError = JsonApiError.create()
        .withCode("xrn:err:platform:internalServerError")
        .withTitle("Internal server error")
        .withStatus("500");

    public static JsonApiError newResourceNotFound(String resourceType, String resourceId) {
        return resourceNotFound
                .withDetail("Resource of type '" + resourceType + "' with id '" + resourceId + "' not found.")
                .withId(java.util.UUID.randomUUID().toString());
    }

    public static JsonApiError newBadRequestError(String message) {
        return badRequest
                .withId(java.util.UUID.randomUUID().toString())
                .withDetail(message);
    }

    public static JsonApiError newInternalServerError() {
        return internalServerError
            .withId(java.util.UUID.randomUUID().toString());
    }
}

Now you could throw a JsonApiErrorsException like:

.orElseThrow(() -> new JsonApiErrorsException(
        CommonErrors.newResourceNotFound("movies", id.toString())));

which would then be converted to a JSON:API error response.

You find implementations of the ExceptionControllerAdvice, JsonApiErrorsException and CommonErrors in the example code.

4. Client Side support

4.1. Deserialization

Simple JSON:API based JSON structures can be deserialized, but only the generic Spring HATEOAS representation models are supported.

For example, a JSON structured like

{
  "data": {
    "id": "1",
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  },
  "links": {
    "self": "http://localhost/movies/1"
  }
}

would be deserialized to an object of class EntityModel<Movie>, where the Movie class looks like

@Data
@NoArgsConstructor
@AllArgsConstructor
@With

public class Movie {
    private String id;
    private String title;
}

Please be aware that the deserialization mechanism is currently not able to deserialize all types of complex JSON:API structures that can be built with JSON:API model builder, but a few things can already be done:

  • Deserialized meta

  • Deserializing included resources into the relationship DTOs

For example, a JSON like

{
  "data": [
    {
      "id": "1",
      "type": "movies",
      "attributes": {
        "title": "The Matrix"
      },
      "relationships": {
        "relatedMovies": {
          "data": {
            "id": "2",
            "type": "movies"
          }
        },
        "directors": {
          "data": [
            {
              "id": "1",
              "type": "directors"
            },
            {
              "id": "2",
              "type": "directors"
            }
          ]
        }
      }
    },
    {
      "id": "3",
      "type": "movies",
      "attributes": {
        "title": "Star Wars"
      },
      "relationships": {
        "directors": {
          "data": {
            "id": "3",
            "type": "directors"
          }
        }
      }
    }
  ],
  "included": [
    {
      "id": "1",
      "type": "directors",
      "attributes": {
        "name": "Lana Wachowski"
      }
    },
    {
      "id": "2",
      "type": "directors",
      "attributes": {
        "name": "Lilly Wachowski"
      }
    },
    {
      "id": "3",
      "type": "directors",
      "attributes": {
        "name": "George Lucas"
      }
    }
  ],
  "links": {
    "self": "http://localhost/movies",
    "first": "http://localhost/movies?page%5Bnumber%5D=0&page%5Bsize%5D=10",
    "prev": "http://localhost/movies?page%5Bnumber%5D=0&page%5Bsize%5D=10",
    "next": "http://localhost/movies?page%5Bnumber%5D=2&page%5Bsize%5D=10",
    "last": "http://localhost/movies?page%5Bnumber%5D=9&page%5Bsize%5D=10"
  },
  "meta": {
    "page": {
      "size": 10,
      "totalElements": 100,
      "totalPages": 10,
      "number": 1
    }
  }
}

would be deserialized in a CollectionModel<EntityModel<MovieWithDirectors>>, so that all the names of the directors are set on the Java side.

CollectionModel<MovieWithDirectors> would NOT resolve the director names, because deserialization of JSON:API specific things like relationships only works with Spring HATEOAS representation models.

More examples for deserialization of relationships you find in the section Deserialization of JSON:API types.

4.2. RestTemplate

If you want to write a client that deserializes server responses into Java objects, you can use RestTemplate, but with a little extra configuration. The following example shows how such a configuration can look like:

@Configuration
@EnableHypermediaSupport(type = {})
static class Config {
    public @Bean
    RestTemplate template() {
        return new RestTemplate();
    }

    @Bean
    public JsonApiMediaTypeConfiguration jsonApiMediaTypeConfiguration(
            ObjectProvider<JsonApiConfiguration> configuration,
            AutowireCapableBeanFactory beanFactory) {
        return new JsonApiMediaTypeConfiguration(configuration, beanFactory);
    }
}

4.2.1. Creating POST requests without serialized JSON:API id

If you want to use RestTemplate to do a POST request, often the JSON:API id will be created by the server and is not part of the JSON body. The easiest way for doing this is to configure a marker value to indicate that DTOs with this id value should not contain the JSON:API id in the resulting JSON.

For example, if you specify a configuration like

new JsonApiConfiguration().withJsonApiIdNotSerializedForValue("doNotSerialize"));

and create a Movie DTO with this id, like

Movie movie = new Movie("doNotSerialize", "Star Wars");

the resulting JSON would look like

{
  "data": {
    "type": "movies",
    "attributes": {
      "title": "Star Wars"
    }
  }
}

4.3. Traverson

The hypermedia type application/vnd.api+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 (application/vnd.api+json) is exposed as a Spring bean.

Alternatively, you can set up and use an instance as follows (source is the exact JSON you saw in the Deserialization section):

LinkDiscoverer linkDiscoverer = new JsonApiLinkDiscoverer();
Links links = linkDiscoverer.findLinksWithRel(SELF, source);

assertThat(links.hasLink("self")).isTrue();
assertThat(links).map(Link::getHref).contains("http://localhost/movies/1");

5. Configuration

There are several options for how to change the output of the JSON:API rendering.

For a specific JSON:API configuration, you can create a Spring bean of type JsonApiConfiguration. Currently, you can configure

Configuration

Descriptions

Default

JsonApiVersionRendered

the JSON:API version should be rendered automatically. This configuration is deprecated, please use the JsonApiObject configuration instead

false

JsonApiObject

Here you can set a JSON:API object do set all allowed properties, like version, ext, profile and meta.

not set

PluralizedTypeRendered

JSON:API types should be rendered as pluralized or non pluralized class names.

pluralized

LowerCasedTypeRendered

JSON:API types should be rendered as lower case or original class names.

lower case

PageMetaAutomaticallyCreated

page information of a PagedModel should be rendered automatically as JSON:API meta.

true

TypeForClass

If a specific Java class should be rendered with a specific JSON:API type. This is useful when representation model classes should get the JSON:API type of the domain model or when derived classes should get the JSON:API type of the superclass.

not set

TypeForClassUsedForDeserialization

if the above "Java class to JSON:API type" mapping should also be used for deserialization. This is very useful for polymorphic use cases.

true

EmptyAttributesObjectSerialized

Empty attributes should be serialized as an empty JSON object, like "attributes": {}. If set to false, no "attributes" key is serialized if attributes are empty.

true

JsonApiIdNotSerializedForValue

A marker value that indicates that the JSON:API id should not be serialized. This is useful if you want to create JSON for a POST request. See also Creating POST requests without serialized JSON:API id.

not set

ObjectMapperCustomizer

A lambda expression to add additional configuration to the Jackson ObjectMapper used for serialization.

not set

JsonApiCompliantLinks

The following links are JSON:API compliant: self, related, describedBy, next, prev, first and last, for top-level links. Only self for resource links. To allow any link, set this configuration to false.

true

JsonApi11LinkPropertiesRemovedFromLinkMeta

If Spring HATEOAS complex links should be serialized/deserialized in a backward compatible (to version 1.x.x of this library) way. By default, the Spring HATEOAS properties title, type and hreflang will then be serialized only as top level link properties, those link properties were introduced in JSON:API 1.1, see jsonapi.org/format/#auto-id—​link-objects. To serialize the Spring HATEOAS properties title, type and hreflang both as top level link properties, as well as in the meta section, set this property to false, see also Links. Be aware that using the default, Spring HATEOAS complex links are rendered in a backward-incompatible way (related to version 1.x.x of this library that only supports JSON:API 1.0), since client might expect properties like title in the meta section.

true

LinksNotUrlEncoded

Set of link relations which are not URL encoded when serializing. Empty by default.

empty set

Since the JSON:API recommendation contains square brackets in the request parameter names, those brackets are usually URL-encoded to %5B and %5D. If you want your server to also interpret raw [ and ] characters in the URL, make sure you provide the following configuration in your Spring application.properties when using Tomcat: server.tomcat.relaxed-query-chars= [,]. If this library automatically creates pagination links, [ and ] characters are always URL-encoded.

Here is an example of how you would implement a JSON:API configuration:

@Bean
JsonApiConfiguration jsonApiConfiguration() {
    return new JsonApiConfiguration()
            .withJsonApiObject(new JsonApiObject(true))
            .withPluralizedTypeRendered(false)
            .withLowerCasedTypeRendered(false)
            .withTypeForClass(MovieRepresentationModelWithoutJsonApiType.class, "my-movies")
            .withTypeForClassUsedForDeserialization(true)
            .withEmptyAttributesObjectSerialized(false)
            .withJsonApiIdNotSerializedForValue("-1")
            .withJsonApi11LinkPropertiesRemovedFromLinkMeta(false)
            .withJsonApiCompliantLinks(false)
            .withObjectMapperCustomizer(
                    objectMapper -> objectMapper.configure(
                            SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true));
}