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")]structTodoResource{id:Uuid,attributes:TodoAttributes,relations:TodoRelations}#[derive(Serialize,Clone)]structTodoAttributes{title:String,done:bool}#[derive(IntoRelationships,FromRelationships,Clone)]structTodoRelations{#[jsonapi(resource_type = "users")]assignee:Option<Uuid>,}structTodoStore{inner:RwLock<BTreeMap<Uuid,Todo>>}implStoreforTodoStore{typeResource = TodoResource;typeCtx = Session<User>;// per-request session, see auth::Session}implShowforTodoStore{typeShown = TodoResource;// plain async fn: Diesel, an HTTP call, whatever — your business.asyncfnshow(&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.