Links and meta¶
This page is about enriching a document beyond the field DSL: attaching free-form
meta, and setting links at the document, resource, relationship and error
levels. The field and relation DSLs already emit the
links and attributes the spec expects by convention — reach here when you need to
add something they don't cover.
meta is free-form, and everywhere¶
meta is a non-standard, free-form array<string, mixed> the spec permits at
every level of a document: the top-level document, each resource object, each
relationship object, each link object, each error object, and the jsonapi
object. The library treats it uniformly: whatever you supply is serialized
as-is, and an empty array is omitted — you never get a "meta": {} you didn't
ask for.
Everything else on this page (links) is structured, but meta is the escape
hatch when the structured members don't have a slot for what you want to say.
Document-level: withMeta / withLinks / withJsonApi¶
Every response value object extends AbstractResponse, so they all carry the
same three document-level withers. Each is immutable — it clones and returns a
new response (the responses convention):
use haddowg\JsonApi\Response\DataResponse;
use haddowg\JsonApi\Schema\JsonApiObject;
use haddowg\JsonApi\Schema\Link\DocumentLinks;
use haddowg\JsonApi\Schema\Link\Link;
$response = DataResponse::fromPage($page, $serializer)
->withMeta(['totalPlays' => 41_204])
->withLinks(DocumentLinks::withBaseUri(
'https://music.example',
self: new Link('/charts/top-100'),
))
->withJsonApi(new JsonApiObject(meta: ['poweredBy' => 'music-catalog']));
| Wither | Sets | Type |
|---|---|---|
withMeta(array $meta) |
top-level meta |
array<string, mixed> |
withLinks(?DocumentLinks $links) |
top-level links |
DocumentLinks or null to clear |
withJsonApi(?JsonApiObject $jsonApi) |
the jsonapi member |
JsonApiObject or null |
When you want a response whose only purpose is to carry meta — no primary
data — use MetaResponse::fromMeta():
use haddowg\JsonApi\Response\MetaResponse;
return MetaResponse::fromMeta(['queued' => true, 'jobId' => 'reindex-7']);
A MetaResponse still accepts withLinks()/withJsonApi() like any other
response.
Resource-level: getMeta / getLinks on a serializer¶
Resource-object meta and links come from the two serializer hooks. On an
AbstractResource both default to "nothing" — getMeta()
returns [] and getLinks() returns null — so a resource emits no custom
meta, and only the by-convention self link (described
below) unless you override them. A bare
AbstractSerializer subclass leaves both methods abstract (they come from
SerializerInterface, and AbstractSerializer only supplies the
TransformerTrait helpers), so it must implement them itself — which is exactly
why the ChartSerializer
writes the no-op defaults out by hand:
// ChartSerializer.php — the two hooks, opted out
public function getMeta(mixed $object, JsonApiRequestInterface $request): array
{
return [];
}
public function getLinks(mixed $object, JsonApiRequestInterface $request): ?ResourceLinks
{
return null;
}
Override them to enrich a resource. Both are request-aware, so the meta or links you emit can depend on the incoming request:
public function getMeta(mixed $object, JsonApiRequestInterface $request): array
{
\assert($object instanceof Album);
return ['ratingCount' => $object->ratingCount];
}
public function getLinks(mixed $object, JsonApiRequestInterface $request): ?ResourceLinks
{
\assert($object instanceof Album);
return ResourceLinks::withBaseUri(
'https://music.example',
self: new Link('/albums/' . $object->id),
links: ['sleeve' => new Link('/albums/' . $object->id . '/sleeve')],
);
}
The conventional resource self link, and the self/related relationship
links, are emitted for you (see below
and relations). You only reach for getLinks() when you want a
self link the library can't derive, or an extra custom relation alongside it — a
self you set here wins over the convention.
Link forms¶
A link serializes one of two ways, and the library picks for you based on whether it carries meta:
- A bare URL string — a
Linkwith nometarenders as a plain string ("https://music.example/charts/top-100"). - A link object — the moment a
Linkcarriesmeta, or you use the richerLinkObject, it renders as an object with anhrefmember.
Link is the base: an href plus optional meta.
new Link('/charts/top-100'); // → bare string
new Link('/charts/top-100', meta: ['period' => '2026-W24']); // → { "href": …, "meta": … }
LinkObject adds the full JSON:API 1.1 link-object vocabulary. Every string
member is optional and omitted when empty:
| Member | Meaning |
|---|---|
href |
the URL (required, first constructor arg) |
rel |
the link's relation type |
title |
a human-readable label |
type |
the media type of the target |
hreflang |
the target's language |
meta |
free-form meta on the link itself |
describedby |
a Link to a description (e.g. a schema) of the target |
use haddowg\JsonApi\Schema\Link\LinkObject;
new LinkObject(
'/albums/42/sleeve',
rel: 'describedby',
title: 'Album artwork',
type: 'image/jpeg',
);
Link containers and the baseUri prepend¶
Links are never set loose — they live in a keyed, construct-only container, one
per level. Each filters out null entries (so an absent relation is simply not
present) and prepends a shared baseUri to every href at render time:
| Container | Level | Reserved keys |
|---|---|---|
DocumentLinks |
top-level document | self, related, first, prev, next, last, profile |
ResourceLinks |
resource object | self |
RelationshipLinks |
relationship object | self, related |
ErrorLinks |
error object | about, type |
DocumentLinks, ResourceLinks and RelationshipLinks each take the reserved
relations as named constructor arguments, plus an arbitrary links map for the
custom relations the spec also permits (ErrorLinks is the exception — it takes
only about and a list of types, with no custom-links map):
use haddowg\JsonApi\Schema\Link\DocumentLinks;
use haddowg\JsonApi\Schema\Link\Link;
DocumentLinks::withBaseUri(
'https://music.example',
self: new Link('/playlists'),
links: ['feed' => new Link('/playlists.atom')],
);
The baseUri prepend lets you store host-relative hrefs and resolve them
against one base. new Link('/playlists') inside a container built with
withBaseUri('https://music.example') renders as
https://music.example/playlists. Each container offers three constructors:
new DocumentLinks($baseUri, …)— the bare constructor,baseUridefaults to''.DocumentLinks::withBaseUri($baseUri, …)— names the base explicitly.DocumentLinks::withoutBaseUri(…)— for fully-qualifiedhrefs, no prepend.
An already-absolute href — a scheme-qualified (https://…) or
protocol-relative (//host/…) URL — passes through untouched, never prefixed,
whatever the base. So a fully-qualified link (a common shape for an error
about/type documentation URL) is safe even when the container carries a base.
For the auto-emitted links below, the base your container is given is resolved by
the response layer: when Server::withBaseUri()
is empty (the default), it is the request origin (<scheme>://<authority>),
so those links render request-absolute; when it is configured, it is that fixed
canonical host (trailing-slash trimmed). A container you build by hand with an
explicit withBaseUri(...) keeps the base you gave it.
Auto-emitted links you don't set by hand¶
Several families of links are populated for you, so you rarely construct a
DocumentLinks, ResourceLinks or RelationshipLinks directly. All are
spec-recommended (SHOULD) and on by default:
- Resource
self— every resource object carrieslinks.self={base}/{uriType}/{id}, the URL to fetch that resource, where{base}is the configured base URI or the request origin. The path segment is the resource'suriType(which defaults to its JSON:APItype), so a resource whose type isbookbut lives at/bookslinks correctly. It is omitted for a resource with an empty id (a not-yet-persisted resource has no self), and agetLinks()selfyou set by hand wins over it. AnAbstractResourceopts out by overridingemitsSelfLink()to returnfalse:
// No by-convention resource self link for this type.
public function emitsSelfLink(): bool
{
return false;
}
-
Top-level document
self— every data/resource document (a single resource, a collection, a related or relationship document, a meta document — but not an error document) carries a top-levellinks.self= the URI that produced it ({base}{request.path}, including the query string on a filtered or sorted request, where{base}is the configured base URI or the request origin). A paginated collection's per-pageself(and aselfyou set withwithLinks()) takes precedence. -
Pagination —
first/prev/next/last(and the per-pageself) are derived from thePagination\Pagewhen you build a collection response withDataResponse::fromPage(). See pagination. -
Relationship
self/related— emitted by convention for every declared relation. See relations.
The DocumentLinks pagination parameters remain available for the rare case of a
hand-built, non-paginated document, but fromPage() is the common path.
Server-level defaults: the jsonapi object¶
When a response doesn't set its own jsonapi member, the library builds one from
two server-level defaults, set once at configuration time:
Server::withVersion(string $version)— thejsonapi.version. Defaults to the spec version the library implements (1.1); set it to''to omit the version member.Server::withDefaultMeta(array $meta)— meta folded into thejsonapiobject on every response.
A response's own withJsonApi() takes precedence over these defaults for that
response. See server for the full configuration surface.
Where profiles fit¶
A profile is the structured way to stamp meta onto every document a server
emits, rather than per response. The worked
TimestampProfile
adds a top-level meta.generatedAt in its finalizeDocument() hook, and the
applied profile URI is echoed in jsonapi.profile automatically. Reach for a
profile over withMeta() when the enrichment is cross-cutting and
negotiation-driven; reach for withMeta() when it's specific to one response.
Profiles are covered in profiles.
Error-level links and meta¶
Error objects carry their own meta and an ErrorLinks container (about plus
one or more type links). You set these on the exception/error rather than on a
response wither — see errors and exceptions for the
error-document shape.
Next / See also¶
- serializers — the
getMeta()/getLinks()hooks in context. - relations — the auto-emitted
self/relatedrelationship links. - pagination — the auto-emitted
first/prev/next/lastlinks. - profiles — cross-cutting meta via
finalizeDocument(). - responses — the response value objects these withers live on.