Paginated Relationships
View SourceAshJsonApi supports pagination for included relationships, allowing you to limit the number of related resources returned when using the include query parameter.
Overview
By default, when you include relationships in a JSON:API request, all related resources are returned. For relationships with many records (e.g., a post with hundreds of comments), this can result in large response payloads and performance issues.
Paginated relationships allow clients to request a specific page of related resources using the included_page query parameter, similar to how top-level resources can be paginated with the page parameter.
Configuration
To enable pagination for specific relationships, add them to the paginated_includes list in your resource's json_api block:
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshJsonApi.Resource]
json_api do
type "post"
# Allow comments to be included
includes [:comments, :author]
# Configure which relationships can be paginated
paginated_includes [:comments]
end
relationships do
has_many :comments, MyApp.Comment
belongs_to :author, MyApp.Author
end
endNested Relationship Paths
You can also configure pagination for nested relationship paths:
defmodule MyApp.Author do
use Ash.Resource,
extensions: [AshJsonApi.Resource]
json_api do
type "author"
includes posts: [:comments]
# Allow pagination for both posts and nested comments
paginated_includes [
:posts,
[:posts, :comments]
]
end
endQuery Parameters
Basic Pagination
Use the included_page query parameter to paginate included relationships:
GET /posts/1?include=comments&included_page[comments][limit]=10This will include only the first 10 comments.
Offset Pagination
Offset pagination uses limit and offset parameters:
GET /posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][offset]=20This returns 10 comments starting from the 21st comment.
Keyset Pagination
Keyset (cursor-based) pagination uses limit, after, and before parameters:
GET /posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][after]=<cursor>Count Parameter
To include the total count of related resources:
GET /posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][count]=trueNested Relationships
For nested relationship paths, use dot notation:
GET /authors/1?include=posts.comments&included_page[posts.comments][limit]=5This paginates the comments included for each post.
Response Format
When a relationship is paginated, the response includes:
- Pagination metadata in the relationship's
metaobject - Pagination links in the relationship's
linksobject
{
"data": {
"id": "1",
"type": "post",
"attributes": {
"title": "My Post"
},
"relationships": {
"comments": {
"data": [
{"id": "1", "type": "comment"},
{"id": "2", "type": "comment"}
],
"links": {
"self": "/posts/1/relationships/comments",
"related": "/posts/1/comments",
"first": "/posts/1?include=comments&included_page[comments][limit]=10",
"next": "/posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][offset]=10",
"prev": null,
"last": "/posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][offset]=40"
},
"meta": {
"limit": 10,
"offset": 0,
"count": 50
}
}
}
},
"included": [
{
"id": "1",
"type": "comment",
"attributes": {
"body": "First comment"
}
},
{
"id": "2",
"type": "comment",
"attributes": {
"body": "Second comment"
}
}
]
}Pagination Links
The links object in a paginated relationship includes:
first: Link to the first page of the relationshipnext: Link to the next page (null if on the last page)prev: Link to the previous page (null if on the first page)last: Link to the last page (only for offset pagination with count)self: Link to the relationship endpoint (if configured)related: Link to the related resources endpoint (if configured)
These links allow clients to navigate through paginated relationships without manually constructing URLs.
Metadata Fields
For offset pagination:
limit: The number of resources requestedoffset: The starting positioncount: The total count (if requested)
For keyset pagination:
limit: The number of resources requestedmore?: Whether there are more resources availablecount: The total count (if requested)
Error Handling
If you attempt to paginate a relationship that is not configured in paginated_includes, you will receive a 400 Bad Request error:
{
"errors": [
{
"status": "400",
"code": "invalid_pagination",
"title": "InvalidPagination",
"detail": "Invalid pagination: Relationship path author is not configured for pagination. Add it to paginated_includes in the resource.",
"source": {
"parameter": "page"
}
}
]
}Best Practices
Performance: Consider adding default limits at the action level for relationships that are commonly included:
read :read do primary? true pagination offset?: true, default_limit: 20 endClient Implementation:
- Use the pagination
linksin the relationship object to navigate pages instead of manually constructing URLs - Check the
metaobject to understand pagination state (limit, offset, count, etc.) - Check if
nextlink isnullto determine if you're on the last page - Check if
prevlink isnullto determine if you're on the first page
- Use the pagination
Nested Pagination: Be cautious with nested pagination - paginating both
postsandposts.commentscan result in complex queries. Consider whether you really need both levels paginated.Backwards Compatibility: Non-paginated includes continue to work as before, so adding
paginated_includesconfiguration is backwards compatible. Clients that don't useincluded_pageparameters will receive all related resources as usual.JSON:API Compliance: The pagination links in relationships follow the JSON:API specification, which states that "A relationship object that represents a to-many relationship MAY also contain pagination links under the links member."
Example: Complete Flow
1. Configure the resource
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshJsonApi.Resource]
json_api do
type "post"
includes [:comments, :author]
paginated_includes [:comments]
routes do
base "/posts"
get :read
index :read
end
end
actions do
defaults [:read]
end
relationships do
has_many :comments, MyApp.Comment
belongs_to :author, MyApp.Author
end
end2. Make the API request
curl "http://localhost:4000/posts/1?include=comments&included_page[comments][limit]=5&included_page[comments][count]=true"
3. Process the response
The response will include:
- The post data in
data - Up to 5 comments in
included - Pagination metadata in
data.relationships.comments.meta - Pagination links in
data.relationships.comments.links - The linkage (comment IDs) in
data.relationships.comments.data
4. Navigate to the next page
curl "http://localhost:4000/posts/1?include=comments&included_page[comments][limit]=5&included_page[comments][offset]=5"
Combining with Other Features
Paginated relationships can be combined with:
- Sparse fieldsets:
fields[comment]=body,created_at - Filtering included:
filter_included[comments][status]=published - Sorting included:
sort_included[comments]=-created_at - Field inputs:
field_inputs[comment][calculated_field][arg]=value
Example combining multiple features:
GET /posts/1?
include=comments&
included_page[comments][limit]=10&
filter_included[comments][status]=published&
sort_included[comments]=-created_at&
fields[comment]=body,author_name