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:

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 UserRole 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.