JSON:API resources (timacdonald/json-api)
Activates automatically when timacdonald/json-api is
installed. This is the package Laravel’s first-party JSON:API resources (added in Laravel 12.45) were
upstreamed from, so if you’re on an earlier Laravel — or simply prefer the package — your resources are
documented the same way.
What it documents
Section titled “What it documents”A resource extending TiMacDonald\JsonApi\JsonApiResource becomes a JSON:API document schema, read
from the same methods the framework’s first-party resources expose:
| Member | Where it comes from |
|---|---|
id, type |
Always emitted as strings — that’s the JSON:API contract, so they’re not read from your toId() / toType() |
attributes |
Analyzed from toAttributes() |
meta |
Analyzed from toMeta(), when you override it |
links |
Emitted when you override toLinks(), as relation-keyed { href, meta? } link objects |
Endpoints returning one of these resources (or a collection of them) also gain the JSON:API include
and fields query parameters, and the response is served as the JSON:API media type
application/vnd.api+json. A paginated collection is documented as a
page, whose links list only the pages that exist, as the
package sends them — an empty list [] on a cursor page with none either side.
class CustomerResource extends JsonApiResource{ public function toAttributes(Request $request): array { return [ 'name' => $this->name, 'email' => $this->email, ]; }}
// app/Http/Controllers/CustomerController.phppublic function show(Customer $customer): CustomerResource{ return new CustomerResource($customer);}"parameters": [ { "name": "fields", "in": "query", "description": "Sparse fieldsets per resource type (fields[TYPE]=field1,field2).", "required": false, "style": "deepObject", "explode": true, "schema": { "type": "object", "additionalProperties": { "type": "string" } } }, { "name": "include", "in": "query", "description": "Comma-separated list of relationships to include as compound-document data.", "required": false, "schema": { "type": "string" } }],"responses": { "200": { "description": "OK", "content": { "application/vnd.api+json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CustomerResource" } }, "required": ["data"] } } } }},// components.schemas — the resource OBJECT, hoisted and reusable"CustomerResource": { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "attributes": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } }, "required": ["name", "email"] } }, "required": ["id", "type"]}Note what’s hoisted: the component is the resource object, not the { data: … } document. That’s
what lets a collection reference the same object per item and wrap the envelope exactly once:
// A collection return — one `data` wrap around an array of resource objects{ "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/CustomerResource" } }, "included": { "type": "array", "items": { "$ref": "#/components/schemas/JsonApiResourceObject" } } }, "required": ["data"]}fields is a single deep-object parameter (fields[TYPE]=field1,field2), not one parameter per
type — the type keys are runtime data, so they’re modeled with additionalProperties.
Relationships and included
Section titled “Relationships and included”toRelationships() is not emitted. Both packages express relationships as closures —
'author' => fn () => new AuthorResource($this->author) — and a static read of a closure can’t produce
JSON:API’s { data: { type, id } } linkage object. Emitting a guess there would document a shape the
resource never returns, so the member is left out until the linkage can be modeled from real
relationship resolution.
Every document does list the compound-document included array, since an endpoint that accepts
include sends one whenever a related resource is loaded. Which resources land in it depends on the
request, so each entry is published as a generic resource object — a shared JsonApiResourceObject
component with a string id and type. Describe it more precisely with a docblock or an
#[Response] attribute if your consumers need it.
The jsonapi object
Section titled “The jsonapi object”When your app registers a server implementation at boot, every document gains a top-level jsonapi
object:
JsonApiResource::resolveServerImplementationUsing( fn () => new ServerImplementation('1.1', ['copyright' => 'Acme Inc.']),);Docuccino publishes it as { version, meta? }. The callback runs per request and may return nothing,
so the member is published as optional. Registering or removing the callback rebuilds the cached
documentation on the next run.
links is handled specially for the same reason: toLinks() returns Link objects keyed by relation,
which a flat analysis can’t see through — so when you override it, Docuccino emits the link-object
shape rather than trying to read your keys:
"links": { "type": "object", "additionalProperties": { "type": "object", "properties": { "href": { "type": "string" }, "meta": { "type": "object" } }, "required": ["href"] }}Which one runs
Section titled “Which one runs”Laravel’s first-party JSON:API resources (Laravel 12.45 and later) are handled by the framework
support; this integration handles the timacdonald/json-api base class. Both feed the same document
builder, so moving from the package to the framework resources leaves the documented resource objects
unchanged. Two things differ, because the two send them differently: the package’s links are
{ href, meta? } link objects where the framework sends what toLinks() returns, and a page of the
package’s resources leaves out the links there is no page for, where the framework sends them as
null. Docuccino documents each as it is sent.
A timacdonald resource is also an Illuminate JsonResource, but the plain
API Resources mapper deliberately declines it — you’ll
never get a flat toArray() shape for a JSON:API resource by accident.
Package versions
Section titled “Package versions”Laravel 12.45 changed how a resource is sent: the response carries what the resource’s
resolveResourceData() returns. The package declares that method, returning its resource object, from
v1.0.0-beta.10. With an older release on Laravel 12.45 or later, your resources are sent as their
attributes alone — no id, no type — so Docuccino documents each one, and each entry in
included, as an object whose shape it doesn’t claim, and warns with
timacdonald-json-api.resource-object-not-sent. Upgrading the package puts the resource objects back.
A resource that overrides resolveResourceData() itself is documented the same way on any release,
since the override decides what is sent.
Configuration
Section titled “Configuration”There’s nothing to configure. The only switch is the shared enabled opt-out, per document, in
docuccino.yaml:
documents: default: integrations: timacdonald_json_api: { enabled: false }With it off, nothing JSON:API-aware runs for these classes: no document schema, no include/fields
parameters, and no application/vnd.api+json media type. Turn it off only if you’d rather document
these endpoints by hand. See the
integrations reference.