Serde types for JSON:API documents, plus (optionally) a deep actix-web integration that turns a resource definition and a hand-written store into spec-compliant CRUD endpoints — routing, query parsing, envelope building, pagination links, content types, and error rendering, all owned by this crate.
Define a resource with the derive macros, implement whichever operation
traits your store supports, mount it. (Lifted from examples/src/main.rs,
which is a complete runnable server — see below.)
#[derive(IntoResponse, ResourceType)]
#[jsonapi(name = "todos")]
struct TodoResource { id: Uuid, attributes: TodoAttributes, relations: TodoRelations }
#[derive(Serialize, Clone)]
struct TodoAttributes { title: String, done: bool }
#[derive(IntoRelationships, FromRelationships, Clone)]
struct TodoRelations {
#[jsonapi(resource_type = "users")]
assignee: Option<Uuid>,
}
struct TodoStore { inner: RwLock<BTreeMap<Uuid, Todo>> }
impl Store for TodoStore {
type Resource = TodoResource;
type Ctx = Session<User>; // per-request session, see auth::Session
}
impl Show for TodoStore {
type Shown = TodoResource;
// plain async fn: Diesel, an HTTP call, whatever — your business.
async fn show(&self, ctx: Session<User>, id: Uuid, q: ShowQuery) -> Result<Self::Shown, Error> { ... }
}
// ...List, Create, Update, Delete similarly.
HttpServer::new(move || {
App::new()
.app_data(store.clone())
.wrap(UserSessionMiddleware::new(DemoUserFactory))
.service(resource::<TodoStore>().show().list().create().update().delete())
})
.bind(("127.0.0.1", 8080))?
.run()
.awaitEach builder method (.show(), .list(), ...) only compiles if the store
implements the matching trait — capability is an implementation, there's no
runtime "not supported" path.
| Feature | Default | Adds |
|---|---|---|
server |
yes | Uuid as a valid resource id type (FromID for Uuid) |
actixweb |
no | Operation traits (Store/Show/List/Create/Update/Delete), route mounting (actix::resource), body extractors, and session middleware (auth) |
diesel |
no | diesel::result::Error → jsonapi::Error mapping (NotFound→404, unique violation→409, FK/check violation→400, else 500) |
- Filters are FLAT top-level query params:
name[eq]=x,name[contains]=y— via [StringMatch];price[gte]=10&price[lte]=20— via [Range<T>]. The namespage,sort, andincludeare reserved (parsed separately) and must not be used as filter field names. Spec-style nesting underfilter[...]is still available, opt-in, by giving your filter type a singlefilter: Innerfield — seeListQuery::parse. sort=-created,name— parses into an orderedVec<(K, Direction)>; unknown keys are a 400, not silently ignoredpage[size]=25&page[after]=<cursor>— the cursor-pagination profile: responselinks.next/links.prevand per-itemmeta.page.cursor, built from theCursorPage::from_probe(LIMIT size+1) idiominclude=author,comments.author— parsed into path segments; whether/how to honor it is the store's choiceapplication/vnd.api+jsonon every response (documents and errors alike)
Every response this crate renders — documents and error documents alike —
carries Content-Type: application/vnd.api+json, per spec. Some HTTP
tooling is strict about Accept: application/json and balks at that media
type even though the body is ordinary JSON. Wrap the app with
[actix::NegotiateContentType] to relabel the header (not the body) to
application/json whenever a request's Accept explicitly prefers it:
App::new().wrap(jsonapi::actix::NegotiateContentType).service(/* ... */)It's opt-in and covers success documents, error documents, extractor failures, and hand-written routes uniformly, since it works at the middleware layer rather than in any one handler.
examples/ is a small standalone crate with a complete "todos" JSON:API
server (cargo run --bin todo-server) wiring the derive macros, the
operation traits, cursor pagination, and session-middleware-based
authorization together end to end. Start there.