Haskell Implementation of the JSON-API specification
Motivation
From the specification itself:
If you’ve ever argued with your team about the way your JSON responses should
be formatted, JSON API can be your anti-bikeshedding tool.
By following shared conventions, you can increase productivity, take advantage
of generalized tooling, and focus on what matters: your application.
Clients built around JSON API are able to take advantage of its features around
efficiently caching responses, sometimes eliminating network requests entirely.
All in all, API discoverability and other HATEOAS
principles make JSON-API an attractive resource serialization option.
The specification
Find the specification here
Example usage
Let's start with an example User record:
data User = User
{ userId :: Int
, userFirstName :: String
, userLastName :: String
} deriving (Eq, Show)
$(deriveJSON defaultOptions ''User)
From this, we can use the json-api
package to produce a payload conformant
to the JSON-API specification like so:
-- Builds the Document which will be serialized as our
-- web server's response payload
mkDocument :: User -> Links -> Document User Text Int
mkDocument usr links =
Document
(Singleton $ toResource usr)
(Just links)
Nothing
-- Helper function to convert a User into a resource object
-- This could be our canonical serialization function for a User in any
-- response payload
toResource :: User -> Resource User Text
toResource user =
Resource resourceId resourceType user resourceLinks resourceMetaData
where
resourceId = ResourceId . pack . show . userId $ user
resourceType = ResourceType "User"
resourceLinks = Just $ userLinks user
resourceMetaData = Nothing
-- helper function to build links for a User resource
userLinks :: User -> Links
userLinks user = toLinks [ ("self", selfLink) ]
where
selfLink = toURL selfPath
selfPath = "/users/" <> (show $ userId user)
When delivered as a response from a web server, for example, we get a payload
that looks like this:
{
"data":{
"attributes":{
"userFirstName":"Isaac",
"userLastName":"Newton",
"userId":1
},
"id":"1",
"meta":null,
"type":"User",
"links":{
"self":"/users/1"
}
},
"meta":null,
"links":{
"self":"/users/1"
}
}
Neat! We can see that if we would like the full User data for the User with
ID=1, we can query /users/1
. Discoverability!
We can also see from the top-level links
data that this particular payload originated
from /users/1
.
This is a very simple example to provide an introduction to the basic idea
behind JSON-API and how to use this library. Check out these examples
for more robust representations of resourceful payloads. Here, you'll start to
see the more comprehensive benefits of a discoverable API.
Let's use the same example User record:
data User = User
{ userId :: Int
, userFirstName :: String
, userLastName :: String
} deriving (Eq, Show)
$(deriveJSON defaultOptions ''User)
Suppose we now have a list of 2 users;
let usrs =
[ User 1 "Isaac" "Newton"
, User 2 "Albert" "Einstein"
]
From this, we can use the json-api
package to produce a payload for a collection with pagination links conformant
to the JSON-API pagination specification like so:
let paginate = Pagination (PageIndex 1) (PageSize 1) (ResourceCount $ toEnum (length usrs))
let resourceLink = (fromJust . importURL) "/users"
let paginationLinks = mkPaginationLinks PageStrategy resourceLink paginate
let doc = mkDocument [head usrs] (Just paginationLinks) Nothing
When delivered as a response from a web server, for example, we get a payload
that looks like this:
{
"data": [
{
"attributes": {
"userFirstName": "Isaac",
"userLastName": "Newton",
"userId": 1
},
"relationships": null,
"id": "1",
"meta": null,
"type": "users",
"links": null
}
],
"meta": null,
"included": [
],
"links": {
"next": "/users?page%5bsize%5d=1&page%5bnumber%5d=2",
"first": "/users?page%5bsize%5d=1&page%5bnumber%5d=1",
"last": "/users?page%5bsize%5d=1&page%5bnumber%5d=2"
}
}
The key function in the code example is mkPaginationLinks
which has the following signature;
mkPaginationLinks :: Strategy -> URL -> Pagination -> Links
Strategy
is a sum type that represents the different paging strategies as laid out in the JSON-API pagination specification. At the time of writing this README, the library only supports 2 paging strategies Offset and Page. Offset is a 0 index based approach unlike Page, i.e. page[offset]
0 is the same as page[number]
1.
The URL
type is used to build the links that appear in the JSON payload. The Pagination
type contains the requisite information for the mkPaginationLinks
function to generate the paging links.
So let's break this example down. To get started we need to create a Pagination
record. The first attribute of the record is PageIndex
. This attribute informs the caller that the page we are looking at is the first in the entire collection (PageIndex
is either a 0 based index or 1 based index depending on the Strategy
). So in our example as we are using PageStrategy
, PageIndex 1
implies we are after the first page. The second attribute of the record is PageSize
. This atrribute tells the caller how many items can appear in the list at most. So in our example seeing there are only 2 users, a PageSize
of 1 would mean that in total we have 2 pages. The third attribute is ResourceCount
. This attribute is required by the function mkPaginationLinks
to figure out which links to generate.
The links object in the JSON payload can have 4 attributes next
, prev
, first
and last
. This library only generates valid links. For example if the request is for the first page of a list, then the prev
link is not present.
Example Project
There is an example project illustrating how the library can be used in the context of a web server.
Hackage
Module documentation can be found on Hackage