1. Overview
In this article, we'll start exploring the JSON-API spec and how that can be integrated into a Spring backed REST API.
We'll use the Katharsis implementation of JSON-API in Java – and we'll set up a Katharsis powered Spring application – so all we need is a Spring application.
2. Maven
First, let's take a look at our maven configuration – we need to add the following dependency into our pom.xml:
<dependency>
<groupId>io.katharsis</groupId>
<artifactId>katharsis-spring</artifactId>
<version>3.0.2</version>
</dependency>
3. A User Resource
Next, let's take a look at our User resource:
@JsonApiResource(type = "users")
public class User {
@JsonApiId
private Long id;
private String name;
private String email;
}
Note that:
- @JsonApiResource annotation is used to define our resource User
- @JsonApiId annotation is used to define the resource identifier
And very briefly – the persistence for this example is going to be a Spring Data repository here:
public interface UserRepository extends JpaRepository<User, Long> {}
4. A Resource Repository
Next, let's discuss our resource repository – each resource should have a ResourceRepositoryV2 to publish the API operations available on it:
@Component
public class UserResourceRepository implements ResourceRepositoryV2<User, Long> {
@Autowired
private UserRepository userRepository;
@Override
public User findOne(Long id, QuerySpec querySpec) {
Optional<User> user = userRepository.findById(id);
return user.isPresent()? user.get() : null;
}
@Override
public ResourceList<User> findAll(QuerySpec querySpec) {
return querySpec.apply(userRepository.findAll());
}
@Override
public ResourceList<User> findAll(Iterable<Long> ids, QuerySpec querySpec) {
return querySpec.apply(userRepository.findAllById(ids));
}
@Override
public <S extends User> S save(S entity) {
return userRepository.save(entity);
}
@Override
public void delete(Long id) {
userRepository.deleteById(id);
}
@Override
public Class<User> getResourceClass() {
return User.class;
}
@Override
public <S extends User> S create(S entity) {
return save(entity);
}
}
A quick note here – this is of course very similar to a Spring controller.
5. Katharsis Configuration
As we are using katharsis-spring, all we need to do is to import KatharsisConfigV3 in our Spring Boot Application:
@Import(KatharsisConfigV3.class)
And configure Katharsis parameters in our application.properties:
katharsis.domainName=http://localhost:8080
katharsis.pathPrefix=/
With that – we can now start consuming the API; for example:
- GET “http://localhost:8080/users“: to get all users.
- POST “http://localhost:8080/users“: to add new user, and more.
6. Relationships
Next, let's discuss how to handle entities relationships in our JSON API.
6.1. Role Resource
First, let's introduce a new resource – Role:
@JsonApiResource(type = "roles")
public class Role {
@JsonApiId
private Long id;
private String name;
@JsonApiRelation
private Set<User> users;
}
And then set up a many-to-many relation between User and Role:
@JsonApiRelation(serialize=SerializeType.EAGER)
private Set<Role> roles;
6.2. Role Resource Repository
Very quickly – here is our Role resource repository:
@Component
public class RoleResourceRepository implements ResourceRepositoryV2<Role, Long> {
@Autowired
private RoleRepository roleRepository;
@Override
public Role findOne(Long id, QuerySpec querySpec) {
Optional<Role> role = roleRepository.findById(id);
return role.isPresent()? role.get() : null;
}
@Override
public ResourceList<Role> findAll(QuerySpec querySpec) {
return querySpec.apply(roleRepository.findAll());
}
@Override
public ResourceList<Role> findAll(Iterable<Long> ids, QuerySpec querySpec) {
return querySpec.apply(roleRepository.findAllById(ids));
}
@Override
public <S extends Role> S save(S entity) {
return roleRepository.save(entity);
}
@Override
public void delete(Long id) {
roleRepository.deleteById(id);
}
@Override
public Class<Role> getResourceClass() {
return Role.class;
}
@Override
public <S extends Role> S create(S entity) {
return save(entity);
}
}
It is important to understand here is that this single resource repo doesn't handle the relationship aspect – that takes a separate repository.
6.3. Relationship Repository
In order to handle the many-to-many relationship between User–Role we need to create a new style of repository:
@Component
public class UserToRoleRelationshipRepository implements RelationshipRepositoryV2<User, Long, Role, Long> {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Override
public void setRelation(User User, Long roleId, String fieldName) {}
@Override
public void setRelations(User user, Iterable<Long> roleIds, String fieldName) {
Set<Role> roles = new HashSet<Role>();
roles.addAll(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}
@Override
public void addRelations(User user, Iterable<Long> roleIds, String fieldName) {
Set<Role> roles = user.getRoles();
roles.addAll(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}
@Override
public void removeRelations(User user, Iterable<Long> roleIds, String fieldName) {
Set<Role> roles = user.getRoles();
roles.removeAll(roleRepository.findAllById(roleIds));
user.setRoles(roles);
userRepository.save(user);
}
@Override
public Role findOneTarget(Long sourceId, String fieldName, QuerySpec querySpec) {
return null;
}
@Override
public ResourceList<Role> findManyTargets(Long sourceId, String fieldName, QuerySpec querySpec) {
final Optional<User> userOptional = userRepository.findById(sourceId);
User user = userOptional.isPresent() ? userOptional.get() : new User();
return querySpec.apply(user.getRoles());
}
@Override
public Class<User> getSourceResourceClass() {
return User.class;
}
@Override
public Class<Role> getTargetResourceClass() {
return Role.class;
}
}
We're ignoring the singular methods here, in the relationship repository.
7. Test
Finally, let's analyze a few requests and really understand what the JSON-API output looks like.
We're going to start retrieving a single User resource (with id = 2):
GET http://localhost:8080/users/2
{
"data":{
"type":"users",
"id":"2",
"attributes":{
"email":"[email protected]",
"username":"tom"
},
"relationships":{
"roles":{
"links":{
"self":"http://localhost:8080/users/2/relationships/roles",
"related":"http://localhost:8080/users/2/roles"
}
}
},
"links":{
"self":"http://localhost:8080/users/2"
}
},
"included":[
{
"type":"roles",
"id":"1",
"attributes":{
"name":"ROLE_USER"
},
"relationships":{
"users":{
"links":{
"self":"http://localhost:8080/roles/1/relationships/users",
"related":"http://localhost:8080/roles/1/users"
}
}
},
"links":{
"self":"http://localhost:8080/roles/1"
}
}
]
}
Takeaways:
- The main attributes of the Resource are found in data.attributes
- The main relationships of the Resource are found in data.relationships
- As we used @JsonApiRelation(serialize=SerializeType.EAGER) for the roles relationship, it is included in the JSON and found in node included
Next – let's get the collection resource containing the Roles:
GET http://localhost:8080/roles
{
"data":[
{
"type":"roles",
"id":"1",
"attributes":{
"name":"ROLE_USER"
},
"relationships":{
"users":{
"links":{
"self":"http://localhost:8080/roles/1/relationships/users",
"related":"http://localhost:8080/roles/1/users"
}
}
},
"links":{
"self":"http://localhost:8080/roles/1"
}
},
{
"type":"roles",
"id":"2",
"attributes":{
"name":"ROLE_ADMIN"
},
"relationships":{
"users":{
"links":{
"self":"http://localhost:8080/roles/2/relationships/users",
"related":"http://localhost:8080/roles/2/users"
}
}
},
"links":{
"self":"http://localhost:8080/roles/2"
}
}
],
"included":[
]
}
The quick take-away here is that we get all Roles in the system – as an array in the data node
8. Conclusion
JSON-API is a fantastic spec – finally adding some structure in the way we use JSON in our APIs and really powering a true Hypermedia API.
This piece explored one way to set it up in a Spring app. But regardless of that implementation, the spec itself is – in my view – very very promising work.
The complete source code for the example is available over on GitHub. It's a Maven project which can be imported and run as-is.